| name | radtools-component-create |
| description | Create components that work with RadTools visual editor and component discovery. Use when building new UI components for projects using RadTools. |
| compatibility | Requires RadTools devtools/ directory and components/ structure |
Create RadTools-Compatible Components
Build components that integrate with RadTools' visual editor, component discovery, and design system.
When to Use This Skill
Use this skill when:
- Creating a new UI component for a RadTools project
- Converting an existing component to be RadTools-compatible
- Adding variants to an existing component
- Setting up component previews in the Components tab
Pre-Flight Check
Before creating a new component:
- Open RadTools (
Shift+Cmd+K) → Components tab - Check if component exists — Can you extend an existing one?
- Check if composable — Can existing components be combined?
Only create new components when necessary.
Component Requirements
RadTools component discovery has strict requirements. All are mandatory.
1. Default Export (Required)
// ✅ Correct - default export
export default function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
// ❌ Wrong - named export (will not be discovered)
export function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
2. Default Prop Values (Required)
The visual editor renders components without props. All optional props must have defaults.
// ✅ Correct - all optional props have defaults in destructuring
interface ButtonProps {
variant?: 'default' | 'outline';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
children: React.ReactNode; // Required props don't need defaults
}
export default function Button({
variant = 'default',
size = 'md',
disabled = false,
children
}: ButtonProps) {
// ...
}
// ❌ Wrong - optional props without defaults (visual editor will break)
export default function Button({
variant, // undefined causes rendering issues
size,
children
}: ButtonProps) {
// ...
}
Note: The component scanner extracts default values from destructuring patterns. Defaults defined elsewhere (e.g., defaultProps) won't be detected.
3. TypeScript Props Interface (Required)
Props must be typed for discovery to extract prop information.
// ✅ Correct - explicit interface
interface CardProps {
title?: string;
children: React.ReactNode;
}
export default function Card({ title = '', children }: CardProps) {
// ...
}
4. Location (Required)
Components must be in /components/ structure:
| Location | Purpose | Tab in RadTools |
|---|---|---|
/components/ui/ |
Design system primitives (Button, Card, Input) | "Design System" tab |
/components/[FolderName]/ |
Grouped components (e.g., /components/Rad_os/) |
Dynamic folder tab |
/components/*.tsx |
Ungrouped project components | Scanned on demand |
Scanner ignores: Files matching .test., .stories., or missing default exports.
5. Design System Tokens Only (Required)
Use tokens from the @theme block in globals.css. Check RadTools → Variables tab for available tokens.
// ✅ Correct - radOS design tokens
<button className="bg-sun-yellow text-black border-black hover:bg-sunset-fuzz">
<div className="bg-cream border border-black rounded-md">
<span className="text-sun-red">Error message</span>
// ❌ Wrong - hardcoded Tailwind palette colors
<button className="bg-yellow-400 text-gray-900">
<button className="bg-[#FCE184]">
Available radOS tokens:
| Category | Tokens |
|---|---|
| Brand | sun-yellow, sky-blue, cream, black, white, warm-cloud, sunset-fuzz, sun-red, green |
| System | success-green, error-red, focus-state |
| Radius | rounded-none, rounded-xs, rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-full |
Component Template
// /components/ui/ComponentName.tsx
interface ComponentNameProps {
variant?: 'default' | 'outline';
size?: 'sm' | 'md' | 'lg';
className?: string;
children: React.ReactNode;
}
export default function ComponentName({
variant = 'default',
size = 'md',
className = '',
children
}: ComponentNameProps) {
const variants = {
default: 'bg-cream border border-black text-black',
outline: 'bg-transparent border-2 border-black text-black',
};
const sizes = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<div className={`rounded-md ${variants[variant]} ${sizes[size]} ${className}`}>
{children}
</div>
);
}
Adding Component Previews
Previews enable visual editing in the Components tab. They load dynamically by folder name.
How Preview Loading Works
/components/Rad_os/ → loads ./previews/Rad_os.tsx
/components/Forms/ → loads ./previews/Forms.tsx
/components/ui/ → uses DesignSystemTab (built-in)
The file name must exactly match the folder name (case-sensitive).
Step 1: Create Preview File
Create /devtools/tabs/ComponentsTab/previews/{FolderName}.tsx:
// /devtools/tabs/ComponentsTab/previews/MyComponents.tsx
// ↑ File name matches folder: /components/MyComponents/
'use client';
import React from 'react';
import ComponentName from '@/components/MyComponents/ComponentName';
import AnotherComponent from '@/components/MyComponents/AnotherComponent';
// Helper for displaying props
function PropsDisplay({ props }: { props: string }) {
return (
<code className="bg-black/5 px-2 py-1 rounded-sm block mt-2 text-sm">
{props}
</code>
);
}
export default function Preview() {
return (
<div className="space-y-6">
{/* Section for ComponentName */}
<div className="p-4 border border-black bg-cream rounded">
<h3 className="mb-3 border-b border-black/20 pb-2">ComponentName</h3>
{/* Base component - edits affect all variants */}
<ComponentName
variant="default"
data-edit-scope="component-definition"
data-component="ComponentName"
>
Default variant
</ComponentName>
<PropsDisplay props="variant='default'" />
{/* Variant - edits affect only this variant */}
<ComponentName
variant="outline"
data-edit-scope="component-definition"
data-component="ComponentName"
data-edit-variant="outline"
>
Outline variant
</ComponentName>
<PropsDisplay props="variant='outline'" />
{/* Size variations - preview only (no data attributes) */}
<div className="flex gap-2 items-center mt-4">
<ComponentName size="sm">Small</ComponentName>
<ComponentName size="md">Medium</ComponentName>
<ComponentName size="lg">Large</ComponentName>
</div>
<PropsDisplay props="size='sm' | 'md' | 'lg'" />
</div>
{/* Section for AnotherComponent */}
<div className="p-4 border border-black bg-cream rounded">
<h3 className="mb-3 border-b border-black/20 pb-2">AnotherComponent</h3>
<AnotherComponent
data-edit-scope="component-definition"
data-component="AnotherComponent"
/>
</div>
</div>
);
}
Step 2: Add Tab (if new folder)
If this is a new component folder, add it to tabs via RadTools UI:
- Open RadTools → Components tab
- Click "+" button to add new folder tab
- Select your folder from dropdown
Or add directly to /devtools/tabs/ComponentsTab/tabConfig.ts:
export const COMPONENT_TABS: ComponentTabConfig[] = [
// ... existing tabs
{
id: 'folder-MyComponents', // Must be 'folder-{FolderName}'
label: 'My Components',
description: 'Description for this component group',
},
];
Edit Scope Attributes
These attributes control what gets edited when using the visual editor:
| Attribute Combo | Effect |
|---|---|
data-edit-scope="component-definition" + data-component="Name" |
Edits base styles (affects all variants) |
Above + data-edit-variant="variantName" |
Edits only that variant's styles |
| No attributes | Preview only, no editing power |
Key points:
- Attributes go directly on the component instance, not wrapper divs
data-componentvalue must match the component's file name (without extension)- Base component (no
data-edit-variant) edits propagate to all variants unless overridden
Troubleshooting
Component not appearing in Components tab?
- Check default export — Must use
export default function - Check location — Must be in
/components/or/components/{FolderName}/ - Check file name — Avoid
.test.tsx,.stories.tsxsuffixes - Click Refresh — Scanner runs on demand, not automatically
- Check console — API errors appear in browser DevTools
Visual editor not rendering component?
- Check default props — All optional props need defaults in destructuring
- Check required props — Provide mock values in preview file
- Check imports — Use
@/components/alias consistently
Edits not persisting?
- Check data attributes — Need all three for variant edits
- Check component path —
data-componentmust match file name - Check NODE_ENV — Must be
development
Validation Checklist
After creating a component:
- Default export
- TypeScript props interface
- Default values for all optional props (in destructuring)
- Located in
/components/or/components/ui/or/components/{Folder}/ - Uses radOS design tokens only (no hardcoded colors)
- Preview file created (named after folder, not component)
- Tab added if new folder
- Component appears in RadTools → Components tab
- Visual editor can render component without errors
Common Patterns
Forwarded Ref (for form components)
import { forwardRef } from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(({
label = '',
error = '',
className = '',
...props
}, ref) => {
return (
<div>
{label && <label className="block text-sm font-medium mb-1">{label}</label>}
<input
ref={ref}
className={`w-full px-3 py-2 border rounded-md bg-white ${error ? 'border-sun-red' : 'border-black'} ${className}`}
{...props}
/>
{error && <p className="text-sm text-sun-red mt-1">{error}</p>}
</div>
);
});
Input.displayName = 'Input';
export default Input;
Compound Component
import { useState, createContext, useContext } from 'react';
interface AccordionContextValue {
isOpen: boolean;
toggle: () => void;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
interface AccordionProps {
children: React.ReactNode;
defaultOpen?: boolean;
}
function Accordion({ children, defaultOpen = false }: AccordionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<AccordionContext.Provider value={{ isOpen, toggle: () => setIsOpen(!isOpen) }}>
<div className="border border-black rounded-md bg-cream">{children}</div>
</AccordionContext.Provider>
);
}
Accordion.Trigger = function AccordionTrigger({ children }: { children: React.ReactNode }) {
const ctx = useContext(AccordionContext);
return (
<button onClick={ctx?.toggle} className="w-full p-3 text-left font-medium text-black">
{children}
</button>
);
};
Accordion.Content = function AccordionContent({ children }: { children: React.ReactNode }) {
const ctx = useContext(AccordionContext);
if (!ctx?.isOpen) return null;
return <div className="p-3 border-t border-black">{children}</div>;
};
export default Accordion;
Polymorphic Component
type PolymorphicProps<T extends React.ElementType> = {
as?: T;
variant?: 'filled' | 'outline';
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'variant' | 'children'>;
export default function Button<T extends React.ElementType = 'button'>({
as,
variant = 'filled',
children,
...props
}: PolymorphicProps<T>) {
const Component = as || 'button';
const variants = {
filled: 'bg-sun-yellow text-black border border-black',
outline: 'bg-transparent border-2 border-black text-black',
};
return (
<Component className={`px-4 py-2 rounded-md ${variants[variant]}`} {...props}>
{children}
</Component>
);
}