coinbase/dev.cds-web
USE THIS when asked to work on a new or existing (WEB) CDS React component in packages/web
npx skills add https://github.com/coinbase/cds --skill dev.cds-web
<!-- TODO: nested AGENTS.md files should work but they seem a little flaky at the moment. Intelligent mdc files are working better -->
Use this guidance when adding ComponentConfigProvider defaults for the specific component you are editing.
packages/web/src/core/componentConfig.ts using its *BaseProps:import type { MyComponentBaseProps } from '../category/MyComponent';
export type ComponentConfig = {
MyComponent?: ConfigResolver<MyComponentBaseProps>;
};
useComponentConfig in the component and destructure from merged props:import { useComponentConfig } from '../hooks/useComponentConfig';
export const MyComponent = memo((_props: MyComponentProps) => {
const mergedProps = useComponentConfig('MyComponent', _props);
const { className, style, ...props } = mergedProps;
return <Box className={className} style={style} {...props} />;
});
_props as the input variable and mergedProps as the configured output.*BaseProps (not polymorphic/full *Props).Use Linaria for zero-runtime CSS. Always use CDS theme CSS variables for colors, spacing, typography, and other design tokens.
Reference packages/web/src/core/theme.ts:53-119 for the CSS variable naming pattern.
import { css, cx } from '@linaria/core';
const containerCss = css`
/* Spacing tokens */
padding: var(--space-2);
gap: var(--space-1);
/* Color tokens */
background: var(--color-bgPrimary);
color: var(--color-fgPrimary);
border: 1px solid var(--color-line);
/* Border radius tokens */
border-radius: var(--borderRadius-400);
/* Typography tokens */
font-size: var(--fontSize-body);
&:hover {
background: var(--color-bgPrimaryHover);
}
`;
// Merge classNames with cx utility **in CORRECT ORDER**
<div
className={cx(
containerCss, // Base styles first
isCompact && conditionClassToApply, // Conditional computed styles
className, // User-provided className prop
classNames.root, // granular overrides last
)}
/>;
IMPORTANT: Using CSS variables ensures components respond correctly to theme changes (light/dark mode, brand themes).
Design tokens from packages/common/src/core/theme.ts map to CSS variables:
--color-{tokenName} (e.g., --color-bgPrimary, --color-fgMuted)--space-{scale} (e.g., --space-2 = 16px, --space-3 = 24px)--fontSize-{font}, --fontWeight-{font}, --lineHeight-{font}, --fontFamily-{font}--borderRadius-{size}, --borderWidth-{size}--iconSize-{size}, --avatarSize-{size}, --controlSize-{name}--shadow-{level}Reference packages/web/src/styles/media.ts for breakpoint values:
Use the Box component's responsive prop API for responsive values:
<Box padding={{ base: 2, phone: 1, desktop: 3 }} />
classNames object prop for granular overrides on child elements within the component.classNames object are specific to the component they should never be on the *BaseProps typedata-active, data-disabled, data-variant, data-filledstyle and styles object props for overriding inline styles.style and styles props should never be on the *BaseProps type.useMemo hook and applied in the correct order (default styles => style prop => styles[ELEMENT_NAME] prop).Example:
type ComponentProps = ComponentBaseProps & {
style?: React.CSSProperties;
styles?: { root?: React.CSSProperties; label?: React.CSSProperties };
};
Use Framer Motion for complex animations:
import { m as motion, AnimatePresence } from 'framer-motion';
<AnimatePresence>
{visible && (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
/>
)}
</AnimatePresence>;
For simple transitions, prefer CSS transitions in Linaria.
Use ARIA attributes:
<div role="group" aria-roledescription="carousel" aria-live="polite">
<button aria-pressed={isActive} tabIndex={isVisible ? 0 : -1} />
</div>
className?: string - CSS class always applied to root elementstyle?: React.CSSProperties - inline styles always applied to root elementas prop for element type (where applicable e.g. see Box)Take coinbase/dev.cds-web from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.