@chakra-ui/react-mcp
Version:
The official MCP server for Chakra UI
1,686 lines (1,623 loc) • 53.9 kB
JavaScript
#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
var server = new McpServer({
name: "chakra-ui",
version: "1.0.0",
capabilities: {
prompts: {},
resources: {},
tools: {}
}
});
// src/lib/uniq.ts
function uniq(array) {
return Array.from(new Set(array));
}
// src/lib/fetch.ts
var CHAKRA_BASE_URL = "https://chakra-ui.com";
var PRO_BASE_URL = "https://pro.chakra-ui.com";
async function fetchJson(url, options, errorContext) {
const response = await fetch(url, options);
if (!response.ok) {
const context = errorContext || `fetch ${url}`;
throw new Error(
`Failed to ${context}: ${response.status} ${response.statusText}`
);
}
return response.json();
}
function createChakraUrl(path) {
return `${CHAKRA_BASE_URL}${path}`;
}
function createProUrl(path) {
return `${PRO_BASE_URL}${path}`;
}
function createAuthHeaders(apiKey) {
return {
headers: {
"x-api-key": apiKey
}
};
}
async function fetchComponentList() {
return fetchJson(
createChakraUrl("/api/types"),
void 0,
"fetch component list"
);
}
async function fetchComponentProps(component) {
return fetchJson(
createChakraUrl(`/api/types/${component}`),
void 0,
`fetch props for component ${component}`
);
}
async function fetchComponentExample(component) {
return fetchJson(
createChakraUrl(`/r/examples/${component}.json`),
void 0,
`fetch example for component ${component}`
);
}
async function fetchProBlocks() {
return fetchJson(
createProUrl("/api/blocks"),
void 0,
"fetch pro blocks"
);
}
async function fetchProBlock(category, id, apiKey) {
if (!apiKey) {
throw new Error("Chakra UI Pro API key is required");
}
return fetchJson(
createProUrl(`/api/blocks/${category}/${id}`),
createAuthHeaders(apiKey),
`fetch pro block ${category}/${id}`
);
}
async function getAllComponentNames() {
const componentList = await fetchComponentList();
return uniq([...componentList.components, ...componentList.charts]);
}
async function getRegularComponentNames() {
const componentList = await fetchComponentList();
return uniq(componentList.components);
}
async function getProBlockContext() {
const blocks = await fetchProBlocks();
return {
categories: blocks.data.map((block) => block.id),
variants: blocks.data.flatMap((block) => block.variants.map((v) => v.id))
};
}
async function fetchTheme() {
return fetchJson(createChakraUrl("/api/theme"), void 0, "fetch theme");
}
async function fetchTokenCategories() {
return fetchJson(
createChakraUrl("/api/theme/tokens"),
void 0,
"fetch theme customization"
);
}
// src/tools/customize-theme.ts
var CUSTOMIZATION_SCENARIOS = {
animations: {
description: `
const config = defineConfig({
theme: {
keyframes: {
// ... keyframes from above
},
tokens: {
animations: {
shakeX: { value: "shakeX 1s ease-in-out infinite" },
},
},
},
})
`
},
breakpoints: {
description: `
const config = defineConfig({
theme: {
breakpoints: {
tablet: "992px",
desktop: "1200px",
wide: "1400px",
},
},
})
`
},
colors: {
description: `
const config = defineConfig({
theme: {
tokens: {
colors: {
brand: {
50: { value: "#e6f2ff" },
100: { value: "#e6f2ff" },
200: { value: "#bfdeff" },
300: { value: "#99caff" },
// ...
950: { value: "#001a33" },
},
},
},
},
})
`
},
conditions: {
description: `
const config = defineConfig({
conditions: {
off: "&:is([data-state=off])",
on: "&:is([data-state=on])",
},
})
`,
usage: `
import { Box } from "@chakra-ui/react"
<Box data-state="off" _off={{ bg: "red.500" }} />
<Box data-state="on" _on={{ bg: "green.500" }} />
`
},
cursor: {
description: `
import { createSystem, defaultConfig } from "@chakra-ui/react"
export const system = createSystem(defaultConfig, {
theme: {
tokens: {
cursor: {
button: { value: "pointer" },
},
},
},
})
`
},
sizes: {
description: `
const config = defineConfig({
theme: {
tokens: {
sizes: {
"1/7": { value: "14.285%" },
"2/7": { value: "28.571%" },
"3/7": { value: "42.857%" },
},
},
},
})
`
},
spacing: {
description: `
const config = defineConfig({
theme: {
tokens: {
spacing: {
"128": { value: "32rem" },
"144": { value: "36rem" },
},
},
},
})
`
},
layerStyles: {
description: `
const config = defineConfig({
theme: {
layerStyles: {
CUSTOM: {
value: {
background: "gray.200",
borderRadius: "md",
padding: "4",
},
},
},
},
})
`
},
textStyles: {
description: `
const config = defineConfig({
theme: {
textStyles: {
CUSTOM: {
value: {
fontSize: "16px",
fontWeight: "bold",
lineHeight: "1.5",
},
},
},
},
})
`
}
};
var generateTemplate = (content) => {
return `
import { createSystem, defaultConfig } from "@chakra-ui/react"
${content}
export const system = createSystem(defaultConfig, config)
`;
};
var fallbackTemplate = (category) => {
return generateTemplate(`
const config = defineConfig({
theme: {
tokens: {
${category}: {
"Token_Key": { value: "Token_Value" },
},
},
},
})
`);
};
var customizeThemeTool = {
name: "customize_theme",
description: "Used to setup a custom theme for your Chakra UI. You can define new tokens or modify existing ones.",
async ctx() {
const tokenCategories = await fetchTokenCategories();
return {
categories: uniq([
...tokenCategories,
...Object.keys(CUSTOMIZATION_SCENARIOS),
"conditions",
"globalCss"
])
};
},
exec(server2, { ctx, name, description }) {
server2.tool(
name,
description,
{
category: z.enum(ctx.categories).describe("The category of the token to customize")
},
async ({ category }) => {
const customizationInfo = Reflect.get(CUSTOMIZATION_SCENARIOS, category);
return {
content: [
{
type: "text",
text: JSON.stringify({
usage: customizationInfo?.usage,
description: customizationInfo ? generateTemplate(customizationInfo.description) : fallbackTemplate(category)
})
},
category === "colors" ? {
type: "text",
text: `
For new colors defined in the theme, YOU MUST create these matching semantic tokens to ensure consistency.
solid, contrast, fg, muted, subtle, emphasized, focusRing. Value can be a string or an object with _light and _dark properties.
const config = defineConfig({
theme: {
tokens: {
colors: {
brand: {
// ...
},
},
},
semanticTokens: {
colors: {
brand: {
solid: { value: "{colors.brand.500}" },
contrast: { value: "{colors.brand.100}" },
fg: { value: {_light:"{colors.brand.700}",_dark:"{colors.brand.600}"} },
muted: { value: "{colors.brand.100}" },
subtle: { value: "{colors.brand.200}" },
emphasized: { value: "{colors.brand.300}" },
focusRing: { value: "{colors.brand.500}" },
},
},
},
},
})
`
} : { type: "text", text: "" },
{
type: "text",
text: `
To get proper autocompletion, YOU MUST run the following command:
npx -y @chakra-ui/cli typegen /PATH_TO_YOUR_THEME.{ts,js}
`
}
]
};
}
);
}
};
var getComponentExampleTool = {
name: "get_component_example",
description: "Retrieve comprehensive example code and usage patterns for a specific Chakra UI component. This tool provides practical implementation examples including basic usage, advanced configurations, and common use cases with complete code snippets.",
async ctx() {
try {
const componentList = await getAllComponentNames();
return { componentList };
} catch (error) {
throw new Error(
`Failed to initialize component example tool: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
},
exec(server2, { ctx, name, description }) {
server2.tool(
name,
description,
{
component: z.enum(ctx.componentList).describe(
"The name of the Chakra UI component to get example code for"
)
},
async ({ component }) => {
try {
const json = await fetchComponentExample(component);
return {
content: [
{
type: "text",
text: JSON.stringify(json)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to fetch example for component ${component}: ${error instanceof Error ? error.message : "Unknown error"}`
}
]
};
}
}
);
}
};
var getComponentPropsTool = {
name: "get_component_props",
description: "Get detailed properties of a specific Chakra UI component. This tool retrieves the properties, attributes, design related props for a component, like size, variant, etc. and configuration options available for a given Chakra UI component.",
async ctx() {
try {
const componentList = await getRegularComponentNames();
return { componentList };
} catch (error) {
throw new Error(
`Failed to initialize component props tool: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
},
exec(server2, { ctx, name, description }) {
server2.tool(
name,
description,
{
component: z.enum(ctx.componentList).describe(
"The name of the Chakra UI component to get properties for"
)
},
async ({ component }) => {
try {
const json = await fetchComponentProps(component);
return {
content: [
{
type: "text",
text: JSON.stringify(json)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to get component props for ${component}: ${error instanceof Error ? error.message : "Unknown error"}`
}
]
};
}
}
);
}
};
var getComponentTemplatesTool = {
name: "get_component_templates",
description: "Retrieve well designed, fully responsive, and accessible component templates.",
disabled(config) {
return !config.apiKey;
},
async ctx() {
try {
return await getProBlockContext();
} catch (error) {
throw new Error(
`Failed to initialize component templates tool: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
},
exec(server2, { ctx, name, description, config }) {
server2.tool(
name,
description,
{
category: z.enum(ctx.categories).describe(
"The name of the block category to retrieve from Chakra UI pro"
),
id: z.enum(ctx.variants).describe(
"The ID of the block variant to retrieve from Chakra UI pro"
)
},
async ({ category, id }) => {
if (!config.apiKey) {
return {
isError: true,
content: [
{
type: "text",
text: JSON.stringify({
error: "Authentication required",
message: "This tool requires a valid Chakra UI Pro key.",
code: "UNAUTHORIZED"
})
}
]
};
}
try {
const json = await fetchProBlock(category, id, config.apiKey);
return {
content: [
{
type: "text",
text: JSON.stringify(json)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to fetch block ${category}/${id}: ${error instanceof Error ? error.message : "Unknown error"}`
}
]
};
}
}
);
}
};
// src/tools/get-theme.ts
var getThemeTool = {
name: "get_theme",
description: `Retrieve the theme specification (colors, fonts, textStyles, etc.) to design any page, component or section`,
exec(server2, { name, description }) {
server2.tool(name, description, async () => {
const json = await fetchTheme();
return {
content: [
{
type: "text",
text: `
- tokens: core theme tokens (non-semantic tokens)
- semanticTokens: context-aware tokens that works automatically for light and dark mode. Use this to remove hard-coded color values.
- layerStyles: consistent container styling
- textStyles: consistent text styling that combines font size, font weight, and line height.
- animationStyles: shorthand for a bunch of animation styles (maps to the animationStyle style prop). When you use this, YOU MUST specify the animation duration and ease.
`
},
{
type: "text",
text: JSON.stringify(json)
}
]
};
});
}
};
var baseSteps = [
{
title: "Install packages",
command: "npm i @chakra-ui/react @emotion/react"
},
{
title: "Add snippets (optional)",
command: "npx @chakra-ui/cli snippet add"
}
];
var tsConfigStep = {
title: "Update TypeScript configuration",
file: "tsconfig.json",
code: `{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"]
}
}
}`
};
var frameworkMap = {
"next-app": {
framework: "Next.js App Router",
nodeVersion: "Node.js 20.x+",
steps: [
...baseSteps,
tsConfigStep,
{
title: "Setup Provider in root layout",
file: "app/layout.tsx",
code: `import { Provider } from "@/components/ui/provider"
export default function RootLayout(props: { children: React.ReactNode }) {
return (
<html suppressHydrationWarning>
<body>
<Provider>{children}</Provider>
</body>
</html>
)
}`
},
{
title: "Optimize bundle (optional)",
file: "next.config.mjs",
code: `export default {
experimental: {
optimizePackageImports: ["@chakra-ui/react"],
},
}`
}
]
},
"next-pages": {
framework: "Next.js Pages Router",
nodeVersion: "Node.js 20.x+",
steps: [
...baseSteps,
tsConfigStep,
{
title: "Setup Provider",
file: "pages/_app.tsx",
code: `import { Provider } from "@/components/ui/provider"
export default function App({ Component, pageProps }: AppProps) {
return (
<Provider>
<Component {...pageProps} />
</Provider>
)
}`
},
{
title: "Update document",
file: "pages/_document.tsx",
code: `import { Head, Html, Main, NextScript } from "next/document"
export default function Document() {
return (
<Html suppressHydrationWarning>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}`
},
{
title: "Optimize bundle (optional)",
file: "next.config.mjs",
code: `export default {
experimental: {
optimizePackageImports: ["@chakra-ui/react"],
},
}`
}
]
},
vite: {
framework: "Vite",
nodeVersion: "Node.js 20.x+",
steps: [
...baseSteps,
{
title: "Update TypeScript configuration",
file: "tsconfig.app.json",
code: `{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"]
}
}
}`
},
{
title: "Setup Provider",
file: "src/main.tsx",
code: `import { Provider } from "@/components/ui/provider"
import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App"
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Provider>
<App />
</Provider>
</React.StrictMode>,
)`
},
{
title: "Install vite-tsconfig-paths",
command: "npm i -D vite-tsconfig-paths"
},
{
title: "Configure Vite paths",
file: "vite.config.ts",
code: `import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
import tsconfigPaths from "vite-tsconfig-paths"
export default defineConfig({
plugins: [react(), tsconfigPaths()],
})`
}
]
},
general: {
framework: "General",
nodeVersion: "Node.js 20.x+",
steps: [
...baseSteps,
tsConfigStep,
{
title: "Setup Provider",
description: "Wrap your application root with the Provider component",
code: `import { Provider } from "@/components/ui/provider"
function App({ Component, pageProps }) {
return (
<Provider>
<Component {...pageProps} />
</Provider>
)
}`
}
]
}
};
var installationTool = {
name: "installation",
description: "Get lightweight installation steps for Chakra UI when using Vite, Next.js App Router, Next.js Pages Router, or general setup.",
exec(server2, { name, description }) {
server2.tool(
name,
description,
{
framework: z.enum(Object.keys(frameworkMap)).describe(
"The framework you're using: vite, next-app, next-pages, or general"
)
},
async ({ framework }) => {
const json = frameworkMap[framework] || frameworkMap.general;
return {
content: [
{
type: "text",
text: JSON.stringify(json, null, 2)
}
]
};
}
);
}
};
// src/tools/list-component-templates.ts
var listComponentTemplatesTool = {
name: "list_component_templates",
description: "List available component templates or blocks in the Chakra UI pro. This tool retrieves the names of all available component templates in the Chakra UI pro, which can be used to enhance the design and functionality of your application.",
disabled(config) {
return !config.apiKey;
},
exec(server2, { name, description }) {
server2.tool(name, description, {}, async () => {
try {
const json = await fetchProBlocks();
return {
content: [
{
type: "text",
text: JSON.stringify(json)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to fetch blocks: ${error instanceof Error ? error.message : "Unknown error"}`
}
]
};
}
});
}
};
// src/tools/list-components.ts
var listComponentsTool = {
name: "list_components",
description: "List all available components in Chakra UI. This tool retrieves the names of all available Chakra UI components.",
async ctx() {
try {
const componentList = await getAllComponentNames();
return { componentList };
} catch (error) {
throw new Error(
`Failed to initialize list components tool: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
},
exec(server2, { ctx, name, description }) {
server2.tool(name, description, {}, async () => {
return {
content: [
{
type: "text",
text: JSON.stringify(ctx.componentList, null, 2)
}
]
};
});
}
};
var MIGRATION_SCENARIOS = {
// ==================== CORE FRAMEWORK & SETUP CHANGES ====================
extend_theme_to_create_system: {
name: "Extend Theme to Create System",
description: "Update theme configuration from extendTheme to createSystem",
before: `import { extendTheme } from "@chakra-ui/react"
export const theme = extendTheme({
fonts: {
heading: \`'Figtree', sans-serif\`,
body: \`'Figtree', sans-serif\`,
},
})`,
after: `import { createSystem, defaultConfig } from "@chakra-ui/react"
export const system = createSystem(defaultConfig, {
theme: {
tokens: {
fonts: {
heading: { value: \`'Figtree', sans-serif\` },
body: { value: \`'Figtree', sans-serif\` },
},
},
},
})`
},
package_json_updates: {
name: "Package JSON Updates",
description: "Remove framer-motion and @emotion/styled from package.jsonn if unused",
before: `npm uninstall @emotion/styled framer-motion`,
after: `The install the latest @emotion/react, next-themes, and @chakra-ui/react packages.`
},
chakra_provider_configuration: {
name: "Provider Configuration",
description: "Update provider setup from ChakraProvider to Provider with snippets",
before: `import { ChakraProvider } from "@chakra-ui/react"
export const App = ({ Component }) => (
<ChakraProvider theme={theme}>
<Component />
</ChakraProvider>
)`,
after: `Use the \`npx @chakra-ui/cli snippet add\` command to add the provider and color-mode snippets.
Then update the app to use the new provider.
import { Provider } from "@/components/ui/provider"
export const App = ({ Component }) => (
<Provider>
<Component />
</Provider>
)
import { ColorModeProvider } from "@/components/ui/color-mode"
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
export function Provider(props) {
return (
<ChakraProvider value={defaultSystem}>
<ColorModeProvider {...props} />
</ChakraProvider>
)
}`
},
color_mode_changes: {
name: "Color Mode Changes",
description: "Replace ColorModeProvider/useColorMode with next-themes",
before: `import {
ColorModeProvider,
useColorMode,
useColorModeValue,
LightMode,
DarkMode,
ColorModeScript
} from "@chakra-ui/react"
const { colorMode, toggleColorMode } = useColorMode()
const bg = useColorModeValue('white', 'gray.800')
<ColorModeProvider>
<LightMode>
<Box>Always light</Box>
</LightMode>
<DarkMode>
<Box>Always dark</Box>
</DarkMode>
</ColorModeProvider>`,
after: `import { useColorMode, useColorModeValue, LightMode, DarkMode } from "@/components/ui/color-mode"
const { colorMode, toggleColorMode } = useColorMode()
const bg = useColorModeValue('white', 'gray.800')
// Use className for forced themes
<Box className="light">
Always light
</Box>
<Box className="dark">
Always dark
</Box>`
},
// ==================== MISSING COMPONENT MIGRATIONS ====================
circular_progress_to_progress_circle: {
name: "CircularProgress to ProgressCircle",
description: "Replace CircularProgress with ProgressCircle compound component, update props: thickness \u2192 --thickness CSS variable, color \u2192 stroke prop on Range",
before: `<CircularProgress
value={75}
thickness="4px"
color="blue.500"
size="lg"
isIndeterminate={false}
/>
<CircularProgress isIndeterminate />`,
after: `<ProgressCircle.Root value={75} size="lg">
<ProgressCircle.Circle css={{ "--thickness": "4px" }}>
<ProgressCircle.Track />
<ProgressCircle.Range stroke="blue.500" />
</ProgressCircle.Circle>
</ProgressCircle.Root>
<ProgressCircle.Root value={null} size="sm">
<ProgressCircle.Circle>
<ProgressCircle.Track />
<ProgressCircle.Range />
</ProgressCircle.Circle>
</ProgressCircle.Root>`
},
progress_stripe_props: {
name: "Progress Stripe Props",
description: "Update Progress stripe properties: hasStripe \u2192 striped, isAnimated \u2192 animated",
before: `<Progress hasStripe value={64} />
<Progress hasStripe isAnimated value={75} colorScheme="blue" />`,
after: `<Progress.Root striped value={64}>
<Progress.Track>
<Progress.Range />
</Progress.Track>
</Progress.Root>
<Progress.Root striped animated value={75} colorPalette="blue">
<Progress.Track>
<Progress.Range />
</Progress.Track>
</Progress.Root>`
},
text_props_changes: {
name: "Text Props Changes",
description: "Update Text component props: noOfLines \u2192 lineClamp, truncated \u2192 truncate",
before: `<Text noOfLines={2}>
"The quick brown fox jumps over the lazy dog" is an English-language pangram
</Text>
<Text truncated>
This text will be truncated with ellipsis
</Text>
<Text noOfLines={[1, 2, 3]}>
Responsive line clamping
</Text>`,
after: `<Text lineClamp={2}>
"The quick brown fox jumps over the lazy dog" is an English-language pangram
</Text>
<Text truncate>
This text will be truncated with ellipsis
</Text>
<Text lineClamp={[1, 2, 3]}>
Responsive line clamping
</Text>`
},
stack_divider_changes: {
name: "Stack Divider Changes",
description: "Replace StackDivider with explicit Stack.Separator components between Stack items",
before: `<VStack divider={<StackDivider borderColor="gray.200" />} spacing={4}>
<Box h="40px" bg="yellow.200">1</Box>
<Box h="40px" bg="tomato">2</Box>
<Box h="40px" bg="pink.100">3</Box>
</VStack>
<HStack divider={<StackDivider />}>
<Text>Item 1</Text>
<Text>Item 2</Text>
<Text>Item 3</Text>
</HStack>`,
after: `<VStack gap={4}>
<Box h="40px" bg="yellow.200">1</Box>
<Stack.Separator borderColor="gray.200" />
<Box h="40px" bg="tomato">2</Box>
<Stack.Separator borderColor="gray.200" />
<Box h="40px" bg="pink.100">3</Box>
</VStack>
<HStack gap={4}>
<Text>Item 1</Text>
<Stack.Separator orientation="vertical" />
<Text>Item 2</Text>
<Stack.Separator orientation="vertical" />
<Text>Item 3</Text>
</HStack>`
},
// ==================== BOOLEAN PROPS STANDARDIZATION ====================
boolean_prop_naming: {
name: "Boolean Prop Naming",
description: "Update boolean prop names: isOpen \u2192 open, isDisabled \u2192 disabled, isRequired \u2192 required, isInvalid \u2192 invalid, defaultIsOpen \u2192 defaultOpen",
before: `<Modal isOpen={isOpen} onClose={onClose}>
<ModalContent>
<ModalHeader>Title</ModalHeader>
</ModalContent>
</Modal>
<Input isDisabled isRequired isInvalid />
<Popover defaultIsOpen />`,
after: `<Modal.Root open={isOpen}>
<Modal.Content>
<Modal.Header>Title</Modal.Header>
</Modal.Content>
</Modal.Root>
<Input disabled required invalid />
<Popover.Root defaultOpen />`
},
button_isActive_prop: {
name: "Button isActive Prop",
description: "Replace isActive prop with data-active attribute",
before: `<Button isActive>Click me</Button>`,
after: `<Button data-active="">Click me</Button>`
},
link_isExternal_prop: {
name: "Link isExternal Prop",
description: "Replace isExternal prop with explicit target and rel props",
before: `<Link isExternal>Click me</Link>`,
after: `<Link target="_blank" rel="noopener noreferrer">
Click me
</Link>`
},
// ==================== COMPONENT-TO-COMPONENT RENAMES ====================
divider_to_separator: {
name: "Divider to Separator",
description: "Rename Divider component to Separator",
before: `import { Divider } from "@chakra-ui/react"
<Divider />
<Divider orientation="vertical" />`,
after: `import { Separator } from "@chakra-ui/react"
<Separator />
<Separator orientation="vertical" />`
},
collapse_to_collapsible: {
name: "Collapse to Collapsible",
description: "Replace Collapse with Collapsible component, change 'in' prop to 'open', wrap content in Collapsible.Content, and replace animateOpacity with keyframes animations",
before: `<Collapse in={isOpen} animateOpacity>
Some content
</Collapse>`,
after: `<Collapsible.Root open={isOpen}>
<Collapsible.Content>Some content</Collapsible.Content>
</Collapsible.Root>`
},
// ==================== STYLE PROPS & THEMING CHANGES ====================
style_props_pseudo_selectors: {
name: "Style Props Pseudo Selectors",
description: "Update pseudo selector prop names: _activeLink \u2192 _currentPage, _activeStep \u2192 _currentStep, _mediaDark \u2192 _osDark, _mediaLight \u2192 _osLight",
before: `<Link _activeLink={{ color: "blue.500" }}>
Active Link
</Link>
<Step _activeStep={{ bg: "blue.100" }}>
Step content
</Step>
<Box _mediaDark={{ bg: "gray.800" }} _mediaLight={{ bg: "white" }}>
Content
</Box>`,
after: `<Link _currentPage={{ color: "blue.500" }}>
Active Link
</Link>
<Step _currentStep={{ bg: "blue.100" }}>
Step content
</Step>
<Box _osDark={{ bg: "gray.800" }} _osLight={{ bg: "white" }}>
Content
</Box>`
},
style_props_apply_removal: {
name: "Apply Prop Removal",
description: "Replace the removed apply prop with textStyle or layerStyle",
before: `<Text apply="textStyles.heading">
Heading text
</Text>
<Box apply="layerStyles.card">
Card content
</Box>`,
after: `<Text textStyle="heading">
Heading text
</Text>
<Box layerStyle="card">
Card content
</Box>`
},
bg_gradient_props: {
name: "Gradient Props",
description: "Gradient style prop split into bgGradient, gradientFrom, and gradientTo props for better performance and type inference",
before: `<Box bgGradient="linear(to-r, red.200, pink.500)" />
<Box bgGradient="linear(to-b, blue.100, blue.500)" />
<Box bgGradient="radial(circle, green.200, green.600)" />`,
after: `<Box bgGradient="to-r" gradientFrom="red.200" gradientTo="pink.500" />
<Box bgGradient="to-b" gradientFrom="blue.100" gradientTo="blue.500" />
<Box bgGradient="circle" gradientFrom="green.200" gradientTo="green.600" />`
},
colorScheme_to_colorPalette: {
name: "ColorScheme to ColorPalette",
description: "Replace 'colorScheme' prop with 'colorPalette' prop for better flexibility and avoiding HTML conflicts",
before: `<Button colorScheme="blue">Click me</Button>
<Alert colorScheme="red">
<AlertIcon />
Alert message
</Alert>
<Badge colorScheme="green">New</Badge>`,
after: `<Button colorPalette="blue">Click me</Button>
<Alert.Root colorPalette="red">
<Alert.Indicator />
Alert message
</Alert.Root>
<Badge colorPalette="green">New</Badge>`
},
stack_spacing_to_gap: {
name: "Stack Spacing to Gap",
description: "In HStack, VStack, and Stack: replace 'spacing' prop with 'gap' prop",
before: `<Stack spacing={4}>
<Box>Item 1</Box>
<Box>Item 2</Box>
</Stack>`,
after: `<Stack gap={4}>
<Box>Item 1</Box>
<Box>Item 2</Box>
</Stack>`
},
// ==================== BUTTON & ICON COMPONENTS ====================
button_icon_to_children: {
name: "Button Icon Props to Children",
description: "Replace 'leftIcon' and 'rightIcon' props with icons as children in Button component",
before: `<Button leftIcon={<Icon />}>
Click me
</Button>
<Button rightIcon={<Icon />}>
Click me
</Button>`,
after: `<Button>
<Icon />
Click me
</Button>
<Button>
Click me
<Icon />
</Button>`
},
icon_button_changes: {
name: "IconButton Changes",
description: "Update IconButton component: remove icon prop (use children), remove isRounded prop (use borderRadius='full')",
before: `<IconButton
icon={<SearchIcon />}
isRounded
aria-label="Search"
/>`,
after: `<IconButton
borderRadius="full"
aria-label="Search"
>
<SearchIcon />
</IconButton>`
},
// ==================== FORM COMPONENTS ====================
form_control_to_field: {
name: "FormControl to Field",
description: "Replace FormControl with Field.Root and related components: FormLabel \u2192 Field.Label, FormHelperText \u2192 Field.HelperText, FormErrorMessage \u2192 Field.ErrorText",
before: `<FormControl isInvalid={isError}>
<FormLabel>Email address</FormLabel>
<Input type='email' />
<FormHelperText>We'll never share your email.</FormHelperText>
<FormErrorMessage>This field is required</FormErrorMessage>
</FormControl>`,
after: `<Field.Root invalid={isError}>
<Field.Label>Email address</Field.Label>
<Input type='email' />
<Field.HelperText>We'll never share your email.</Field.HelperText>
<Field.ErrorText>This field is required</Field.ErrorText>
</Field.Root>`
},
pin_input_changes: {
name: "PinInput Changes",
description: "Update PinInput: value/defaultValue use string[] instead of string, onChange \u2192 onValueChange, onComplete \u2192 onValueComplete, add PinInput.Control and PinInput.Label",
before: `<PinInput defaultValue="123" onChange={onChange} onComplete={onComplete}>
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>`,
after: `<PinInput.Root defaultValue={["1", "2", "3"]} onValueChange={onValueChange} onValueComplete={onValueComplete}>
<PinInput.Label>Enter PIN</PinInput.Label>
<PinInput.Control>
<PinInput.Input index={0} />
<PinInput.Input index={1} />
<PinInput.Input index={2} />
</PinInput.Control>
</PinInput.Root>`
},
number_input_changes: {
name: "NumberInput Changes",
description: "Update NumberInput: rename components to NumberInput.Control, NumberInput.IncrementTrigger, NumberInput.DecrementTrigger, onChange \u2192 onValueChange, onInvalid \u2192 onValueInvalid, remove parse/format props",
before: `<NumberInput onChange={onChange} onInvalid={onInvalid} parse={parse} format={format}>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>`,
after: `<NumberInput.Root onValueChange={onValueChange} onValueInvalid={onValueInvalid} formatOptions={formatOptions}>
<NumberInput.Input />
<NumberInput.Control>
<NumberInput.IncrementTrigger />
<NumberInput.DecrementTrigger />
</NumberInput.Control>
</NumberInput.Root>`
},
checkbox_component_changes: {
name: "Checkbox Component Changes",
description: "Refactor Checkbox to use compound components with Root, HiddenInput, Control, Indicator, and Label",
before: `<Checkbox defaultChecked>Checkbox</Checkbox>`,
after: `<Checkbox.Root defaultChecked>
<Checkbox.HiddenInput />
<Checkbox.Control>
<Checkbox.Indicator />
</Checkbox.Control>
<Checkbox.Label>Checkbox</Checkbox.Label>
</Checkbox.Root>`
},
radio_group_component_changes: {
name: "Radio Group Component Changes",
description: "Refactor RadioGroup to use compound components with Root, Item, ItemHiddenInput, ItemIndicator, and ItemText",
before: `<RadioGroup defaultValue="2">
<Radio value="1">Radio</Radio>
<Radio value="2">Radio</Radio>
</RadioGroup>`,
after: `<RadioGroup.Root defaultValue="2">
<RadioGroup.Item value="1">
<RadioGroup.ItemHiddenInput />
<RadioGroup.ItemIndicator />
<RadioGroup.ItemText />
</RadioGroup.Item>
</RadioGroup.Root>`
},
// ==================== SLIDER COMPONENTS ====================
slider_props_changes: {
name: "Slider Props Changes",
description: "Update Slider: onChange \u2192 onValueChange, onChangeEnd \u2192 onValueChangeEnd, remove onChangeStart and isReversed props",
before: `<Slider
defaultValue={30}
onChange={onChange}
onChangeStart={onChangeStart}
onChangeEnd={onChangeEnd}
isReversed
>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb />
</Slider>`,
after: `<Slider.Root
defaultValue={[30]}
onValueChange={onValueChange}
onValueChangeEnd={onValueChangeEnd}
>
<Slider.Control>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumb index={0} />
</Slider.Control>
</Slider.Root>`
},
range_slider_to_slider: {
name: "RangeSlider to Slider",
description: "Replace RangeSlider with Slider component that accepts array values",
before: `<RangeSlider defaultValue={[10, 30]}>
<RangeSliderTrack>
<RangeSliderFilledTrack />
</RangeSliderTrack>
<RangeSliderThumb index={0} />
<RangeSliderThumb index={1} />
</RangeSlider>`,
after: `<Slider.Root defaultValue={[10, 30]}>
<Slider.Control>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumbs />
</Slider.Control>
</Slider.Root>`
},
// ==================== MODAL & DIALOG COMPONENTS ====================
modal_to_dialog: {
name: "Modal to Dialog",
description: "Replace Modal with Dialog component and update props: isOpen \u2192 open, onClose \u2192 onOpenChange, isCentered \u2192 placement='center'",
before: `<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent>
<ModalHeader>Title</ModalHeader>
<ModalCloseButton />
<ModalBody>
Content goes here
</ModalBody>
<ModalFooter>
<Button onClick={onClose}>Close</Button>
</ModalFooter>
</ModalContent>
</Modal>`,
after: `<Dialog.Root open={isOpen} onOpenChange={onOpenChange}>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Title</Dialog.Title>
</Dialog.Header>
<Dialog.Body>
Content goes here
</Dialog.Body>
<Dialog.Footer>
<Button onClick={onClose}>Close</Button>
</Dialog.Footer>
<Dialog.CloseTrigger />
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>`
},
dialog_or_drawer_props_changes: {
name: "Dialog and Drawer Props",
description: "Update Dialog and Drawer props: isOpen \u2192 open, onChange \u2192 onOpenChange, blockScrollOnMount \u2192 preventScroll, closeOnEsc \u2192 closeOnEscape, closeOnOverlayClick \u2192 closeOnInteractOutside, initialFocusRef \u2192 initialFocusEl function, finalFocusRef \u2192 finalFocusEl function",
before: `<Dialog
isOpen={isOpen}
onChange={onChange}
blockScrollOnMount={true}
closeOnEsc={true}
closeOnOverlayClick={true}
initialFocusRef={initialFocusRef}
finalFocusRef={finalFocusRef}
>
Content
</Dialog>`,
after: `<Dialog.Root
open={isOpen}
onOpenChange={onOpenChange}
preventScroll={true}
closeOnEscape={true}
closeOnInteractOutside={true}
initialFocusEl={() => initialFocusRef.current}
finalFocusEl={() => finalFocusRef.current}
>
<Dialog.Content>Content</Dialog.Content>
</Dialog.Root>`
},
// ==================== SELECTION COMPONENTS ====================
select_to_native_select: {
name: "Select to Native Select",
description: "Replace Select with NativeSelect component using the new compound component pattern",
before: `<Select placeholder='Select option' icon={<ChevronDownIcon />}>
<option value='option1'>Option 1</option>
<option value='option2'>Option 2</option>
<option value='option3'>Option 3</option>
</Select>`,
after: `<NativeSelect.Root size="sm" width="240px">
<NativeSelect.Field placeholder="Select option">
<option value='option1'>Option 1</option>
<option value='option2'>Option 2</option>
<option value='option3'>Option 3</option>
</NativeSelect.Field>
<NativeSelect.Indicator>
<ChevronDownIcon />
</NativeSelect.Indicator>
</NativeSelect.Root>`
},
menu_component_changes: {
name: "Menu Component Changes",
description: "Update Menu component to use compound components: Menu \u2192 Menu.Root, MenuButton \u2192 Menu.Trigger, MenuList \u2192 Menu.Content, MenuItem \u2192 Menu.Item, wrap in Portal and Menu.Positioner",
before: `<Menu>
<MenuButton as={Button} rightIcon={<ChevronDownIcon />}>
Actions
</MenuButton>
<MenuList>
<MenuItem>Download</MenuItem>
<MenuItem>Create a Copy</MenuItem>
</MenuList>
</Menu>`,
after: `<Menu.Root>
<Menu.Trigger asChild>
<Button>
Actions
<ChevronDownIcon />
</Button>
</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.Item value="download">Download</Menu.Item>
<Menu.Item value="copy">Create a Copy</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>`
},
menu_context_changes: {
name: "Menu Context Changes",
description: "Update Menu render prop pattern to use Menu.Context instead of render prop function",
before: `<Menu>
{({ isOpen }) => (
<>
<MenuButton isActive={isOpen} as={Button} rightIcon={<ChevronDownIcon />}>
{isOpen ? "Close" : "Open"}
</MenuButton>
<MenuList>
<MenuItem>Download</MenuItem>
<MenuItem onClick={() => alert("Kagebunshin")}>Create a Copy</MenuItem>
</MenuList>
</>
)}
</Menu>`,
after: `<Menu.Root>
<Menu.Context>
{(menu) => (
<Menu.Trigger asChild>
<Button>
{menu.open ? "Close" : "Open"}
<ChevronDownIcon />
</Button>
</Menu.Trigger>
)}
</Menu.Context>
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.Item value="download">Download</Menu.Item>
<Menu.Item value="copy" onSelect={() => alert("Kagebunshin")}>
Create a Copy
</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>`
},
menu_option_group_changes: {
name: "Menu Option Group Changes",
description: "Update MenuOptionGroup to use Menu.RadioItemGroup and Menu.CheckboxItemGroup for separate state handling",
before: `<Menu>
<MenuButton as={Button}>Trigger</MenuButton>
<MenuList>
<MenuOptionGroup defaultValue="asc" title="Order" type="radio">
<MenuItemOption value="asc">Ascending</MenuItemOption>
<MenuItemOption value="desc">Descending</MenuItemOption>
</MenuOptionGroup>
<MenuDivider />
<MenuOptionGroup title="Country" type="checkbox">
<MenuItemOption value="email">Email</MenuItemOption>
<MenuItemOption value="phone">Phone</MenuItemOption>
<MenuItemOption value="country">Country</MenuItemOption>
</MenuOptionGroup>
</MenuList>
</Menu>`,
after: `<Menu.Root>
<Menu.Trigger asChild>
<Button>Trigger</Button>
</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content minW="10rem">
<Menu.RadioItemGroup defaultValue="asc">
<Menu.RadioItem value="asc">Ascending</Menu.RadioItem>
<Menu.RadioItem value="desc">Descending</Menu.RadioItem>
</Menu.RadioItemGroup>
<Menu.CheckboxItemGroup defaultValue={["email"]}>
<Menu.CheckboxItem value="email">Email</Menu.CheckboxItem>
<Menu.CheckboxItem value="phone">Phone</Menu.CheckboxItem>
<Menu.CheckboxItem value="country">Country</Menu.CheckboxItem>
</Menu.CheckboxItemGroup>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>`
},
// ==================== NAVIGATION & LAYOUT COMPONENTS ====================
tabs_component_changes: {
name: "Tabs Component Changes",
description: "Update Tabs component structure: Tab \u2192 Tabs.Trigger, TabList \u2192 Tabs.List, TabPanel \u2192 Tabs.Content, TabPanels removed, value prop required on triggers and content",
before: `<Tabs>
<TabList>
<Tab>One</Tab>
<Tab>Two</Tab>
<Tab>Three</Tab>
</TabList>
<TabPanels>
<TabPanel>one!</TabPanel>
<TabPanel>two!</TabPanel>
<TabPanel>three!</TabPanel>
</TabPanels>
</Tabs>`,
after: `<Tabs.Root>
<Tabs.List>
<Tabs.Trigger value="one">One</Tabs.Trigger>
<Tabs.Trigger value="two">Two</Tabs.Trigger>
<Tabs.Trigger value="three">Three</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="one">one!</Tabs.Content>
<Tabs.Content value="two">two!</Tabs.Content>
<Tabs.Content value="three">three!</Tabs.Content>
</Tabs.Root>`
},
tabs_props_changes: {
name: "Tabs Props Changes",
description: "Update Tabs props: defaultIndex \u2192 defaultValue, index \u2192 value, onChange \u2192 onValueChange, isLazy \u2192 lazyMount and unmountOnExit",
before: `<Tabs defaultIndex={0} index={0} onChange={(index) => {}} isLazy />`,
after: `<Tabs.Root defaultValue={0} value={0} onValueChange={({ value }) => {}} lazyMount unmountOnExit />`
},
accordion_component_changes: {
name: "Accordion Component Changes",
description: "Update Accordion component: allowMultiple \u2192 multiple, allowToggle \u2192 collapsible, index \u2192 value, defaultIndex \u2192 defaultValue, AccordionButton \u2192 Accordion.Trigger, AccordionIcon \u2192 Accordion.ItemIndicator",
before: `<Accordion allowMultiple index={[0]} onChange={() => {}}>
<AccordionItem>
<AccordionButton>Section 1 title</AccordionButton>
<AccordionPanel>Panel content</AccordionPanel>
</AccordionItem>
</Accordion>`,
after: `<Accordion multiple value={["0"]} onValueChange={() => {}}>
<AccordionItem>
<Accordion.Trigger>Section 1 title</Accordion.Trigger>
<AccordionPanel>Panel content</AccordionPanel>
</AccordionItem>
</Accordion>`
},
// ==================== CONTENT DISPLAY COMPONENTS ====================
table_component_renames: {
name: "Table Component Renames",
description: "Update Table components: TableContainer \u2192 Table.ScrollArea, Td/Th \u2192 Table.Cell/Table.ColumnHeader, isNumeric \u2192 textAlign='end'",
before: `<TableContainer>
<Table variant="simple">
<TableCaption>Imperial to metric conversion factors</TableCaption>
<Thead>
<Tr>
<Th>Product</Th>
<Th isNumeric>Price</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>Item</Td>
<Td isNumeric>$25.00</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>`,
after: `<Table.ScrollArea>
<Table.Root size="sm">
<Table.Header>
<Table.Row>
<Table.ColumnHeader>Product</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">Price</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>Item</Table.Cell>
<Table.Cell textAlign="end">$25.00</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
</Table.ScrollArea>`
},
avatar_props_changes: {
name: "Avatar Changes",
description: "Update Avatar component: move image props to Avatar.Image, move fallback to Avatar.Fallback, move name prop to Avatar.Fallback",
before: `<Avatar
name="John Doe"
src="/avatar.jpg"
fallbackSrc="/fallback.jpg"
size="lg"
>
<AvatarBadge boxSize="1.25em" bg="green.500" />
</Avatar>`,
after: `<Avatar.Root size="lg">
<Avatar.Image src="/avatar.jpg" />
<Avatar.Fallback name="John Doe" />
</Avatar.Root>`
},
tag_component_changes: {
name: "Tag Component Changes",
description: "Update Tag component structure: Tag \u2192 Tag.Root, TagLabel \u2192 Tag.Label, TagLeftIcon \u2192 Tag.StartElement, TagRightIcon \u2192 Tag.EndElement, TagCloseButton \u2192 Tag.CloseTrigger",
before: `<Tag>
<TagLeftIcon boxSize="12px" as={AddIcon} />
<TagLabel>Cyan</TagLabel>
<TagRightIcon boxSize="12px" as={AddIcon} />
</Tag>
<Tag>
<TagLabel>Green</TagLabel>
<TagCloseButton />
</Tag>`,
after: `<Tag.Root>
<Tag.StartElement>
<AddIcon />
</Tag.StartElement>
<Tag.Label>Cyan</Tag.Label>
<Tag.EndElement>
<AddIcon />
</Tag.EndElement>
</Tag.Root>
<Tag.Root>
<Tag.Label>Green</Tag.Label>
<Tag.CloseTrigger />
</Tag.Root>`
},
alert_component_changes: {
name: "Alert Component Changes",
description: "Update Alert component structure: AlertIcon \u2192 Alert.Indicator, wrap content in Alert.Content, and use compound component pattern with Alert.Root",
before: `<Alert>
<AlertIcon />
<AlertTitle>Your browser is outdated!</AlertTitle>
<AlertDescription>Your Chakra experience may be degraded.</AlertDescription>
</Alert>`,
after: `<Alert.Root status="error">
<Alert.Indicator />
<Alert.Content>
<Alert.Title>Invalid Fields</Alert.Title>
<Alert.Description>
Your form has some errors. Please fix them and try again.
</Alert.Description>
</Alert.Content>
</Alert.Root>`
},
// ==================== UTILITY & INTERACTIVE COMPONENTS ====================
spinner_props: {
name: "Spinner Props",
description: "Update Spinner component props: thickness \u2192 borderWidth, speed \u2192 animationDuration",
before: `<Spinner thickness="2px" speed="0.5s" />`,
after: `<Spinner borderWidth="2px" animationDuration="0.5s" />`
},
skeleton_component_changes: {
name: "Skeleton Component Changes",
description: "Update Skeleton component: startColor and endColor props now use CSS variables, isLoaded prop is now loading with inverted boolean logic",
before: `<Skeleton startColor="pink.500" endColor="orange.500" />
<Skeleton isLoaded>
<span>Chakra ui is cool</span>
</Skeleton>`,
after: `<Skeleton
css={{
"--start-color": "colors.pink.500",
"--end-color": "colors.orange.500",
}}
/>
<Skeleton loading={false}>
<span>Chakra ui is coo