name: component-scaffold description: Scaffold a new design system component with all required files, Ark UI integration, Storybook stories (docs/controls), optional Vitest browser tests, and barrel export wiring. Use when the user asks to create, scaffold, or add a new component.
Component Scaffold Skill
Scaffold a new design system component following all project conventions.
Input
The user provides a component name (e.g., "tooltip", "progress-bar"). Normalize it to kebab-case for file names and PascalCase for code identifiers.
- File prefix:
ds-{name}(e.g.,ds-tooltip) - Component name:
Ds{Name}(e.g.,DsTooltip) - Directory:
packages/design-system/src/components/ds-{name}/
Steps
Step 1: Check Ark UI for an existing primitive
Before writing any code, check if Ark UI already provides this component:
- Call the Ark UI MCP
list_componentstool withframework: "react". - If a matching component exists:
- Call
get_component_propswithframework: "react"and the component name to get the full API. - Call
get_examplewithframework: "react", the component name, andexampleId: "basic"to get reference code. - Call
styling_guidewith the component name to get data attributes for SCSS. - Use the Ark primitive as the base. Do NOT expose all Ark props -- create an own props layer.
- Do NOT duplicate Ark internal state with
useState.
- Call
- If no matching component exists, build a custom implementation.
Step 2: Create the component files
All files go in packages/design-system/src/components/ds-{name}/ (plus optional __tests__/ for browser tests).
ds-{name}.types.ts
import type { CSSProperties, ReactNode, Ref } from 'react';
export const ds{Name}Variants = ['...'] as const;
export type Ds{Name}Variant = (typeof ds{Name}Variants)[number];
export interface Ds{Name}Props {
// Value props first
ref?: Ref<HTMLElement>;
className?: string;
style?: CSSProperties;
children?: ReactNode;
// Slot props
locale?: { /* any hardcoded strings */ };
// Callbacks last
onChange?: (value: unknown) => void;
}
Follow these rules:
- Value/config props first, then slot/render props, then callbacks last.
- Export variant arrays as
as constfor storybook argTypes. - Use
Ref<HTMLElement>for the ref type. - Add
localeprop if the component has any hardcoded user-facing text.
ds-{name}.tsx
import classNames from 'classnames';
import styles from './ds-{name}.module.scss';
import type { Ds{Name}Props } from './ds-{name}.types';
const Ds{Name} = ({
ref,
className,
style,
children,
...rest
}: Ds{Name}Props) => {
return (
<div
ref={ref}
className={classNames(styles.root, className)}
style={style}
{...rest}
>
{children}
</div>
);
};
export default Ds{Name};
Follow these rules:
- No
forwardRef-- passrefas a regular prop. - Use
classNamesfor conditional classes, not template literals. - No unnecessary
useMemooruseCallback. - If wrapping an Ark UI primitive, hook into its callbacks to forward to own props. Do not mirror its internal state.
ds-{name}.module.scss
.root {
display: flex;
align-items: center;
}
Follow these rules:
- Use design tokens:
--color-*,--spacing-*,--font-size-*. - No hardcoded colors or spacing.
- No
!important. - No comments (unless genuinely complex).
- Use
0.2sfor transitions. - Use
[data-focus-visible]for focus states,[data-disabled]or&:disabledfor disabled. - Keep component-specific CSS variables in this file, not in
_root.scss. - Use
@mixinfor repeated patterns.
ds-{name}.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import Ds{Name} from './ds-{name}';
import { ds{Name}Variants } from './ds-{name}.types';
const meta: Meta<typeof Ds{Name}> = {
title: 'Components/{Name}',
component: Ds{Name},
parameters: {
layout: 'centered',
},
argTypes: {
variant: { control: 'select', options: ds{Name}Variants },
className: { table: { disable: true } },
style: { table: { disable: true } },
ref: { table: { disable: true } },
},
};
export default meta;
type Story = StoryObj<typeof Ds{Name}>;
export const Default: Story = {
args: {},
};
Follow these rules:
- Stories are for documentation and controls; behavior is tested in
*.browser.test.tsx(see below). - Import variant arrays from types for
argTypes.options. - Hide internal args (
className,style,ref) withtable: { disable: true }. - No inline styles -- use
*.stories.module.scssif needed. - Add stories for: Default, each variant, Disabled, Controlled (if applicable), Localized (if has
localeprop).
__tests__/ds-{name}.browser.test.tsx (recommended)
Use Vitest browser mode (vitest/browser). Spy callbacks with vi.fn(); query with page.getByRole, getByLabelText, getByText (not getByTestId unless unavoidable).
import { describe, expect, it, vi } from 'vitest';
import { page } from 'vitest/browser';
import Ds{Name} from '../ds-{name}';
describe('Ds{Name}', () => {
it('renders and responds to interaction', async () => {
const onChange = vi.fn();
await page.render(<Ds{Name} onChange={onChange} />);
// await expect.element(page.getByRole('...')).toBeVisible();
// await page.getByRole('button').click();
// expect(onChange).toHaveBeenCalled();
});
});
See existing examples under packages/design-system/src/components/*/__tests__/*.browser.test.tsx.
index.ts
export { default as Ds{Name} } from './ds-{name}';
export type { Ds{Name}Props } from './ds-{name}.types';
Note: the barrel file uses .ts extension, not .tsx.
Step 3: Wire the barrel export
Add the new component to packages/design-system/src/index.ts in alphabetical order:
export * from './components/ds-{name}';
Step 4: Validate
Run the following commands from the workspace root:
pnpm eslint packages/design-system/src/components/ds-{name}/
pnpm --filter @drivenets/design-system typecheck
If you added __tests__/ds-{name}.browser.test.tsx:
pnpm --filter @drivenets/design-system test packages/design-system/src/components/ds-{name}/__tests__/ds-{name}.browser.test.tsx --run
Fix any errors before finishing.