This document defines the coding conventions for the shadcn-solid-components project. Follow these rules to ensure consistency when adding new components or modifying existing ones.
src/
├── components/<name>/ # Primitive components (shadcn/ui primitive layer)
│ ├── index.tsx # Component code (required)
│ ├── _metadata.json # Metadata (required)
│ ├── index.css # Custom CSS (only when Tailwind alone is insufficient)
│ └── locales/ # Locale files (only when component has user-facing text)
├── hoc/<name>/ # Higher-order / composed components
│ ├── index.tsx # Component code (required)
│ ├── _metadata.json # Metadata (required)
│ └── locales/ # Locale files (required when component has user-facing text)
│ ├── en-US.ts
│ ├── ja-JP.ts
│ ├── zh-CN.ts
│ └── zh-TW.ts
├── lib/ # Utility functions, hooks, shared logic
├── i18n/ # i18n type definitions and global locale aggregation
│ ├── types.ts
│ └── locales/
├── themes/ # CSS theme presets
│ ├── base.css
│ ├── default.preset.css
│ └── supabase.preset.css
components/— Primitive layer. Wraps headless primitives (@kobalte/core,@ark-ui/solid,@corvu/*, etc.) with styling. Must not depend on othercomponents/orhoc/modules.hoc/— Composition layer. Composes multiplecomponents/primitives into higher-level UI patterns. Must import fromcomponents/, not from headless primitives directly.lib/— Shared utilities. No UI rendering logic.themes/— CSS presets only. No TypeScript.
All components that accept an as prop must use the polymorphic generic pattern:
export type ButtonProps<T extends ValidComponent = 'button'> = ComponentProps<
typeof ButtonPrimitive<T>
> &
VariantProps<typeof buttonVariants>
export const Button = <T extends ValidComponent = 'button'>(props: ButtonProps<T>) => {
// ...
}For components without an as prop (e.g. simple div wrappers), use plain types:
export type CardProps = ComponentProps<'div'>
export const Card = (props: CardProps) => {
// ...
}Always separate custom props from rest props using splitProps. The destructured rest is spread onto the primitive element:
const [, rest] = splitProps(props as ButtonProps, ['class', 'variant', 'size'])When mergeProps is needed for defaults, split from the merged object:
const merge = mergeProps({ showCloseButton: true } as DialogContentProps, props)
const [, rest] = splitProps(merge, ['class', 'children', 'showCloseButton'])Every sub-component's root element must include a data-slot attribute for CSS selector targeting:
<DialogPrimitive.Content data-slot="dialog-content" ...>
<AccordionPrimitive.Item data-slot="accordion-item" ...>Naming convention: data-slot="<component>-<sub>" (e.g. dialog-content, accordion-trigger).
Use cx() from shadcn-solid-components/lib/cva to compose classes in this exact order:
class={cx(
buttonVariants({ variant: props.variant, size: props.size }), // 1. Variant styles (or base Tailwind classes)
'rounded-component', // 2. Theme-aware border-radius
componentClass, // 3. Theme override from useComponentClass
props.class, // 4. User class (always last)
)}For components without variants, the first argument is the base Tailwind class string:
class={cx(
'bg-card text-card-foreground flex flex-col gap-6 border py-6 shadow-sm',
'rounded-component',
componentClass,
props.class,
)}Components that support theme overrides must call useComponentClass:
import { ComponentName } from 'shadcn-solid-components/lib/theme-context'
import { useComponentClass } from 'shadcn-solid-components/lib/theme-helpers'
const componentClass = useComponentClass(ComponentName.Button, props as ButtonProps)Components with visual variants (e.g. size, color) must define variants using cva() and export them:
import { cva } from 'shadcn-solid-components/lib/cva'
export const buttonVariants = cva({
base: ['inline-flex items-center justify-center ...'],
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-white hover:bg-destructive/90',
// ...
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 gap-1.5 px-3',
// ...
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
})The base array format is preferred over a single string for readability when multiple logical groups exist.
Icons embedded directly in components must follow this pattern:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 6L6 18M6 6l12 12"
/>
</svg>Attributes: fill="none", stroke="currentColor", stroke-width="2", stroke-linecap="round", stroke-linejoin="round".
When a primitive sub-component needs no styling changes, re-export it directly:
export const DialogPortal = DialogPrimitive.PortalWhen it only needs a data-slot, wrap it minimally:
export const Dialog = (props: DialogProps) => {
return <DialogPrimitive data-slot="dialog" {...props} />
}HOCs must build on components/ layer primitives. Never import headless primitives directly:
// ✅ Correct
import { AlertDialog, AlertDialogContent } from 'shadcn-solid-components/components/alert-dialog'
import { buttonVariants } from 'shadcn-solid-components/components/button'
// ❌ Wrong
import { Dialog as AlertDialogPrimitive } from '@kobalte/core/alert-dialog'Any HOC with user-visible text must support localization:
- Define the locale type in
src/i18n/types.ts(e.g.ConfirmDialogLocale) - Create locale files in
locales/directory (en-US, ja-JP, zh-CN, zh-TW) - Read global locale via
useLocale(), fallback to built-in default:
const locale = (): ConfirmDialogLocale => ({
...defaultLocale, // Built-in default
...useLocale().ConfirmDialog, // Global override from ConfigProvider
...dialogState()?.locale, // Per-call override
})Each locale file must:
- Import the type from
shadcn-solid-components/i18n/types - Export a named constant using camelCase locale code
// locales/en-US.ts
import type { ConfirmDialogLocale } from 'shadcn-solid-components/i18n/types'
export const enUS: ConfirmDialogLocale = {
confirm: 'Continue',
cancel: 'Cancel',
}// locales/zh-CN.ts
import type { ConfirmDialogLocale } from 'shadcn-solid-components/i18n/types'
export const zhCN: ConfirmDialogLocale = {
confirm: '确认',
cancel: '取消',
}Naming: file = kebab-case (en-US.ts), export = camelCase (enUS, zhCN, zhTW, jaJP).
After adding a new HOC with locale support, update the global locale files in src/i18n/locales/:
// src/i18n/locales/en-US.ts
import { enUS as ConfirmDialog } from 'shadcn-solid-components/hoc/confirm-dialog/locales/en-US'
// ... add to the enUS: Locale objectAlso add the field to the Locale interface in src/i18n/types.ts.
For HOCs that need imperative invocation (e.g. confirm()), use the signal + Promise pattern:
const [dialogState, setDialogState] = createSignal<(Options & ResolveReject) | null>(null)
export function confirm(options: Options): Promise<boolean> {
return new Promise<boolean>(resolve => {
setDialogState({ ...options, resolve })
})
}The rendering component reads from the signal and resolves the promise on user action.
Use // ============ comment blocks to separate logical sections:
// ============================================================================
// Types
// ============================================================================
// ============================================================================
// State
// ============================================================================
// ============================================================================
// Imperative API
// ============================================================================
// ============================================================================
// Component
// ============================================================================| Source | Import Style | Example |
|---|---|---|
| Cross-module internal | shadcn-solid-components/... alias |
import { cx } from 'shadcn-solid-components/lib/cva' |
| Same directory | Relative path | import { Button } from '../button' |
| Locale in same module | Relative path | import { enUS as defaultLocale } from './locales/en-US' |
| Third-party | Package name | import { Dialog } from '@kobalte/core/dialog' |
| SolidJS | Package name | import { splitProps } from 'solid-js' |
Never use relative paths for cross-module imports (e.g. do not use ../../lib/cva — use shadcn-solid-components/lib/cva).
When adding a new component, you must update all three files:
Add an entry to the ComponentName object:
export const ComponentName = {
// ...existing entries
MyNewComponent: 'MyNewComponent',
} as constAdd a type import and a mapping entry:
import type { MyNewComponentProps } from '../components/my-new-component'
export interface ComponentPropsMap {
// ...existing entries
[ComponentName.MyNewComponent]: MyNewComponentProps
}Add an export path:
{
"exports": {
"./components/my-new-component": "./src/components/my-new-component/index.tsx"
}
}Every component and HOC must include a _metadata.json file with these fields:
{
"name": "components/button",
"displayName": "Button",
"description": "The primary interactive element for triggering actions. Provides six visual variants...",
"category": "components",
"useCases": [
"Primary form submission, save, cancel, and next-step actions",
"Destructive variant for dangerous actions such as delete or remove",
"Icon variants for compact toolbar buttons",
"Ghost and link variants for low-emphasis actions"
],
"usage": "import { Button, buttonVariants } from \"shadcn-solid-components/components/button\"",
"tags": ["components", "button", "solidjs", "ui"],
"dependencies": ["@kobalte/core", "cva"]
}| Field | Type | Description |
|---|---|---|
name |
string |
Module path: components/<name> or hoc/<name> |
displayName |
string |
Human-readable name (PascalCase) |
description |
string |
English description of functionality |
category |
string |
"components" or "hoc" |
useCases |
string[] |
3–4 typical usage scenarios |
usage |
string |
Import example code |
tags |
string[] |
Searchable tags |
dependencies |
string[] |
Third-party npm package names the component depends on (no relative imports, no version specifiers) |
Use Tailwind CSS utility classes as the primary styling mechanism. Only write custom CSS when Tailwind cannot express the desired style.
- Animation
@keyframesdefinitions - Complex pseudo-class / attribute selectors (e.g.
[data-pinned-border="left"]) - CSS variable references that Tailwind cannot generate
- Component-specific utility definitions
Custom CSS files must be named index.css and placed in the component directory. Import them in the component:
import './index.css'Always use the theme-aware border-radius utilities instead of hardcoded Tailwind values:
rounded-component— all cornersrounded-t-component— top cornersrounded-b-component— bottom cornersrounded-l-component— left cornersrounded-r-component— right corners
These resolve to var(--radius-component) which respects the theme's base.radius setting.
When custom CSS needs to target specific sub-components, use [data-slot="..."] selectors:
[data-slot="tanstack-table-resize-handle"]:hover {
background-color: var(--color-border);
}Use the dark: variant prefix. The project defines a custom variant:
@custom-variant dark (&:is([data-kb-theme="dark"] *));Do not use @media (prefers-color-scheme: dark).
The project uses Biome for formatting and linting. Key settings (from biome.json and .editorconfig):
| Setting | Value |
|---|---|
| Indent | 2 spaces |
| Line ending | LF |
| Line width | 100 characters |
| JS quote style | Single quotes |
| JSX attribute quotes | Double quotes |
| Semicolons | As needed (omit when safe) |
| Trailing commas | Always |
| Arrow parens | As needed (omit for single param) |
| Charset | UTF-8 |
| Trim trailing whitespace | Yes |
| Insert final newline | Yes |
Run formatting and linting before committing:
pnpm format # biome format --write ./src ./dev
pnpm lint # biome check --write ./src && tsc --noEmit- Test files live in the
test/directory at the project root - Naming:
<name>.test.ts(x) - Client tests: jsdom environment, run with
pnpm test:client - SSR tests: node environment, file must be named
server.test.ts(x), run withpnpm test:ssr - All tests:
pnpm test
When adding a new component, ensure all of the following are completed:
-
src/components/<name>/index.tsx— Component implementation -
src/components/<name>/_metadata.json— Metadata file -
src/lib/theme-context.ts— Add entry toComponentNameobject -
src/lib/component-props-map.ts— Add type import andComponentPropsMapentry -
package.json— Add entry toexportsfield - If the component has visual variants: export
xxxVariantsfor external use - If the component needs custom CSS: create
index.cssin the component directory
When adding a new HOC, additionally:
- If the HOC has user-facing text: add locale type to
src/i18n/types.ts - Create
locales/directory with all four locale files (en-US, ja-JP, zh-CN, zh-TW) - Update all global locale files in
src/i18n/locales/ - Add the locale field to the
Localeinterface insrc/i18n/types.ts