| name | creating-gel-component |
| description | Scaffolds a new GEL design system UI component following project conventions. Use when creating a new component, adding a component, or scaffolding a component in packages/ui. |
Creating a GEL Component
Scaffolds a new React component in packages/ui/src/components/. All conventions (file naming, imports, patterns) are defined in the packages/ui/.agents/AGENTS.md — follow those automatically.
When to Use
Use this skill when you want to create a new GEL design system component. This includes scaffolding the necessary files (component, styles, types, tests, stories), following the established patterns for responsive variants and Tailwind styling, and registering the component in the main index.
Steps
- Create the types file (
{name}.types.ts)
import { HTMLAttributes } from 'react';
import { type VariantProps } from 'tailwind-variants';
import { ResponsiveVariants } from '../../types/responsive-variants.types.js';
import { styles } from './{kebab-case-name}.styles.js';
type Variants = VariantProps<typeof styles>;
export type {PascalName}Props = {
variantProp?: ResponsiveVariants<Variants['variantProp']>;
} & HTMLAttributes<HTMLElement>;
- Create the styles file (
{name}.styles.ts)
import { tv } from 'tailwind-variants';
export const styles = tv({
base: '',
variants: {},
});
- Create the component file (
{name}.component.tsx)
'use client';
import React, { forwardRef, Ref } from 'react';
import { mergeProps, useFocusRing } from 'react-aria';
import { useBreakpoint } from '../../hook/breakpoints.hook.js';
import { resolveResponsiveVariant } from '../../utils/breakpoint.util.js';
import { styles as componentStyles } from './{kebab-case-name}.styles.js';
import { type {PascalName}Props } from './{kebab-case-name}.types.js';
function Base{PascalName}(
{ className, variant = 'default', ...props }: {PascalName}Props,
ref: Ref<HTMLDivElement>,
) {
const breakpoint = useBreakpoint();
const { isFocusVisible, focusProps } = useFocusRing();
return (
<div
ref={ref}
className={componentStyles({
className,
variant: resolveResponsiveVariant(variant, breakpoint),
isFocusVisible,
})}
{...mergeProps(props, focusProps)}
/>
);
}
export const {PascalName} = forwardRef(Base{PascalName});
- Create the index file (
index.ts)
export { {PascalName} } from './{kebab-case-name}.component.js';
export { type {PascalName}Props } from './{kebab-case-name}.types.js';
- Create the test file (
{name}.spec.tsx)
import { render } from '@testing-library/react';
import { {PascalName} } from './{kebab-case-name}.component.js';
describe('{PascalName}', () => {
it('renders the component', () => {
const { container } = render(<{PascalName} />);
expect(container).toBeInTheDocument();
});
});
See the writing-gel-tests skill for comprehensive testing guidance.
- Create the stories file (
{name}.stories.tsx)
import { type Meta, StoryFn, type StoryObj } from '@storybook/react-vite';
import { {PascalName} } from './{kebab-case-name}.component.js';
const meta: Meta<typeof {PascalName}> = {
title: 'Components/{PascalName}',
component: {PascalName},
tags: ['autodocs'],
decorators: [(Story: StoryFn) => <Story />],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {},
};
Interactive stories using useState can be defined as function components:
export const Example = () => {
const [state, setState] = useState(initialValue);
return <Component prop={state} onChange={setState} />;
};
- Register the component
Add the export to packages/ui/src/components/index.ts:
export * from './{kebab-case-name}/index.js';
- Verify
Run these commands from packages/ui/:
pnpm vitest run src/components/{kebab-case-name} — Run tests
pnpm build — Verify build