A CLI tool that converts React shadcn/ui components to Svelte 5 and Vue 3 using a two-stage pipeline with Mitosis as the intermediate representation.
- React to Svelte 5: Full support for Svelte 5 runes (
$props,$bindable,$derived) - React to Vue 3: Composition API with
<script setup>syntax - CVA Support: Preserves class-variance-authority patterns
- forwardRef Handling: Converts to framework-native ref patterns
- Radix Primitives: Extracts props from
@radix-ui/*components - cn() Utility: Maintains Tailwind class merging patterns
- Multi-component Files: Handles files with multiple exported components
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Convert a component to Vue
pnpm rcc convert ./button.tsx -t vue -o ./Button.vue
# Convert a component to Svelte
pnpm rcc convert ./button.tsx -t svelte -o ./Button.svelte
# Convert all components in a file
pnpm rcc convert ./card.tsx -t vue -aReact TSX → preParse plugins (ts-morph) → parseJsx() → postParse → componentToX() → postGenerate → Output
react-component-converter/
├── modules/
│ ├── core/ # Converter, plugins, Mitosis integration
│ ├── parser/ # React component parsing (ts-morph)
│ └── cli/ # Command-line interface
├── playground/ # React components for testing
└── demos/
├── vue-demo/ # Vue 3 demo app
└── svelte-demo/ # Svelte 5 demo app
Convert React components directly to target framework:
# Single component output
pnpm rcc convert ./Button.tsx -t vue -o ./Button.vue
pnpm rcc convert ./Button.tsx -t svelte -o ./Button.svelte
# All components in file (outputs to directory)
pnpm rcc convert ./Card.tsx -t vue -a -o ./components/
# Options
-t, --target <framework> Target framework: vue | svelte
-o, --output <path> Output file or directory
-a, --all Convert all components in file
-v, --verbose Enable verbose output
--no-typescript Output JavaScript instead of TypeScript
--svelte-version <version> Svelte version: 4 | 5 (default: 5)Parse React component and output intermediate representation:
pnpm rcc parse ./Button.tsx
pnpm rcc parse ./Button.tsx -o ./button.ir.jsonCompile Mitosis IR to target framework:
pnpm rcc compile ./button.ir.json -t vue
pnpm rcc compile ./button.ir.json -t svelte -o ./Button.svelte// Input: React
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
destructive: "bg-destructive text-destructive-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);CVA definitions are preserved in both Vue and Svelte output with proper TypeScript types.
// Input: React
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, ...props }, ref) => (
<button ref={ref} className={cn(buttonVariants(), className)} {...props} />
)
);<!-- Output: Vue -->
<script setup lang="ts">
const elementRef = ref<HTMLButtonElement | null>(null);
defineExpose({ elementRef });
</script>
<template>
<button ref="elementRef" :class="cn(buttonVariants(), props.class)" v-bind="$attrs">
<slot />
</button>
</template><!-- Output: Svelte -->
<script lang="ts">
let { ref = $bindable(null), ...restProps }: Props = $props();
</script>
<button bind:this={ref} class={cn(buttonVariants(), className)} {...restProps}>
{@render children?.()}
</button>Components using @radix-ui/* primitives automatically have their props extracted:
// Input: React (Switch using @radix-ui/react-switch)
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root ref={ref} className={cn(...)} {...props}>
<SwitchPrimitives.Thumb className={cn(...)} />
</SwitchPrimitives.Root>
));The converter automatically extracts props like checked, onCheckedChange, disabled from the Radix primitive definitions and generates proper state handling:
<!-- Output: Svelte -->
<script lang="ts">
let { checked, onCheckedChange, disabled, ...restProps }: Props = $props();
const dataState = $derived(checked ? 'checked' : 'unchecked');
function toggle() {
if (!disabled) {
checked = !checked;
onCheckedChange?.(checked);
}
}
</script>
<button type="button" role="switch" aria-checked={checked} data-state={dataState} onclick={toggle}>
<span data-state={dataState}>...</span>
</button>The cn() utility for Tailwind class merging is preserved:
// Input
cn("base-class", variant && "variant-class", className)
// Output (both frameworks)
cn("base-class", variant && "variant-class", className)Both demos include the cn utility at @/lib/utils (Vue) or $lib/utils (Svelte).
React TSX
↓
[ts-morph Analysis]
├── Find component definitions (forwardRef, arrow functions)
├── Extract CVA configurations
├── Analyze props from TypeScript interfaces
├── Detect Radix primitive usage
├── Categorize imports
└── Transform code for Mitosis compatibility
↓
Transformed code + metadata
Transformed code
↓
[Mitosis parseJsx()]
↓
MitosisComponent (IR)
MitosisComponent
↓
[componentToSvelte() / componentToVue()]
↓
[postGenerate plugins (e.g., Svelte5RunesPlugin)]
↓
[Prettier formatting]
↓
Vue SFC or Svelte 5 Component
interface PropDefinition {
name: string;
type: string;
optional: boolean;
defaultValue?: unknown;
isVariant: boolean; // From CVA variants
allowedValues?: string[]; // Variant options
isStateProp?: boolean; // Controls data-state attribute
dataStateValues?: { // For Radix state props
true: string;
false: string;
};
}interface CvaConfig {
name: string; // e.g., "buttonVariants"
baseClasses: string;
variants: Record<string, Record<string, string>>;
defaultVariants: Record<string, string>;
compoundVariants?: Array<{
conditions: Record<string, string>;
classes: string;
}>;
}interface ReactComponentMeta {
cva?: CvaConfig;
forwardRef?: {
elementType: string; // e.g., "HTMLButtonElement"
paramName: string; // e.g., "ref"
};
usesCn: boolean;
originalImports: OriginalImport[];
}Converter engine using Mitosis with plugins:
- Converter:
convert()andconvertAll()functions - Plugins:
react-analyzer-plugin- ts-morph analysis for CVA, forwardRef, cn patternsshadcn-plugin- shadcn/ui-specific metadata extractionsvelte5-runes-plugin- Svelte 5 runes syntax transformation
- Types:
PropDefinition,CvaConfig,ExtendedMitosisComponent, etc. - Mappings: Radix primitive props, icon mappings, framework equivalents
Key transformations for Vue:
React.forwardRef→ref()+defineExpose()- Props →
defineProps<Props>()+withDefaults() className→:classbinding- Children →
<slot />
Key transformations for Svelte:
React.forwardRef→$bindable()+bind:this- Props →
$props()destructuring className→classattribute- Children →
{@render children?.()} - State derivation →
$derived()
React component analysis using ts-morph (used by react-analyzer-plugin):
- Analyzers:
cva.ts- Extract CVA definitionsforward-ref.ts- Detect forwardRef patternsprops.ts- Extract props from interfaces/typesimports.ts- Categorize and transform imports
Commander.js-based CLI with commands:
parse- Output IR JSONcompile- Compile IR to frameworkconvert- Direct conversion (parse + compile)
cd demos/vue-demo
pnpm devcd demos/svelte-demo
pnpm dev# Convert all playground components to Vue
for file in playground/src/components/ui/*.tsx; do
name=$(basename "$file" .tsx)
Name=$(echo "$name" | sed -r 's/(^|-)([a-z])/\U\2/g')
pnpm rcc convert "$file" -t vue -o "demos/vue-demo/src/lib/components/ui/$Name.vue" -a
done
# Convert all playground components to Svelte
for file in playground/src/components/ui/*.tsx; do
name=$(basename "$file" .tsx)
Name=$(echo "$name" | sed -r 's/(^|-)([a-z])/\U\2/g')
pnpm rcc convert "$file" -t svelte -o "demos/svelte-demo/src/lib/components/ui/$Name.svelte" -a
doneEdit modules/core/src/mappings/radix-props.ts:
export const RADIX_PRIMITIVE_PROPS: RadixPrimitivePropsConfig = {
'@radix-ui/react-new-component': {
Root: [
{ name: 'open', type: 'boolean', optional: true, isStateProp: true,
dataStateValues: { true: 'open', false: 'closed' } },
{ name: 'onOpenChange', type: '(open: boolean) => void', optional: true },
],
},
};Edit modules/core/src/mappings/icons.ts:
export const iconMappings: Record<string, IconMapping> = {
Check: {
source: 'lucide-react',
svelte: 'lucide-svelte',
vue: 'lucide-vue-next',
},
};- Complex Hooks: Custom React hooks are not converted; they need manual rewriting
- Context: React Context is not automatically converted to Svelte context or Vue provide/inject
- Compound Components: Components with complex parent-child relationships may need manual adjustment
- Radix Primitives: Full Radix behavior requires using equivalent libraries (bits-ui for Svelte, radix-vue for Vue)
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm test
# Type check
pnpm typecheck
# Lint
pnpm lintMIT