| name | backpack-external-component-migration |
| description | Migrate React components from external Skyscanner repositories (e.g., carhire-homepage)
into Backpack design system components. Use when: (1) Component exists in app-specific
repo with "unstable_backpack" or similar prefix, (2) Component needs to be promoted to
official Backpack component, (3) Converting app code to follow Backpack constitution,
(4) Extracting reusable UI patterns from product repos. Covers GitHub API access,
Backpack naming conventions, modern Sass API, TypeScript patterns, license headers,
accessibility testing, and Storybook integration. MANDATORY: Component must pass full
test suite (npm run lint && npm run check-react-versions && npm run check-bpk-dependencies
&& npm run jest) with 0 errors before acceptance.
|
| author | Claude Code |
| version | 1.4.0 |
| date | "2026-03-02T00:00:00.000Z" |
| changelog | v1.4.0 (2026-03-02):
- Fixed index.ts template: use explicit `import ... + export default` instead of `export { default }` shorthand
- Simplified README template to match standard Backpack format (button/chip pattern)
- README now only includes: Installation + Usage + Props (removed Tracking, Accessibility, Design tokens, Features sections)
- Added "What NOT to include in README" guidance
- Removed README Tracking section from verification checklist
v1.3.0 (2026-02-28):
- Added mandatory data-backpack-ds-component attribute requirement
- Updated TypeScript template to include getDataComponentAttribute usage
- Added README Tracking section template
- Added to verification checklist
v1.2.0 (2026-02-28):
- Closed props interface by default: no className, no HTML element spread
- Added "Why no HTML element spread?" rationale
- Updated TypeScript template to reflect lean props API
- Updated test/example templates to match
- Updated API Encapsulation notes section
v1.1.0 (2026-02-12):
- Added mandatory full test suite acceptance criteria
- Enhanced verification phase with comprehensive debugging steps
- Added common acceptance failure patterns and solutions
- Clarified that 100% component coverage is required
- Added detailed lint failure troubleshooting
- Documented proper handling of generated directories in .eslintignore
- Added guidance for undefined Sass token errors
- Expanded verification checklist with accessibility requirements
v1.0.0 (2026-02-12):
- Initial skill creation from BpkThinking component migration
- Complete workflow from external repo to Backpack standards
|
Backpack External Component Migration
Problem
Components developed in product-specific repositories (like carhire-homepage) need to be
converted into proper Backpack design system components that follow strict architectural
conventions, accessibility standards, and design system patterns.
Context / Trigger Conditions
Use this workflow when:
- Component exists in another Skyscanner repo with path like
unstable_backpack/ComponentName
- Product team wants to contribute component back to Backpack
- Component has been validated in production and ready for design system inclusion
- Need to standardize an existing component to Backpack standards
- Converting one-off UI patterns into reusable design system components
Common indicators:
- Component has
unstable_backpack in its path
- Uses basic Backpack components but doesn't follow full Backpack structure
- Has product-specific dependencies that need to be removed
- Missing required Backpack files (accessibility tests, proper documentation)
- Not following Backpack naming conventions or file structure
Prerequisites
- GitHub CLI access configured (
gh auth status)
- Backpack repository cloned locally
- Repository documentation access:
CLAUDE.md and AGENTS.md for project context
constitution.md (in .specify/memory/)
decisions/ directory for architectural decisions
- Design approval from Backpack squad (required before starting)
Solution
Phase 1: Discovery & Research (15-20 mins)
1.1 Extract External Component
gh api "repos/Skyscanner/[repo-name]/contents/[path-to-component]" \
--jq '.[] | {name: .name, type: .type, path: .path}'
for file in Component.tsx Component.module.scss Component.test.tsx \
Component.stories.tsx index.ts; do
gh api "repos/Skyscanner/[repo-name]/contents/[path]/$file" \
--jq '.content' | base64 -d > /tmp/$file
done
1.2 Review Backpack Standards
cat .specify/memory/constitution.md
ls decisions/
cat decisions/modern-sass-api.md
cat decisions/component-scss-filenames.md
cat decisions/accessibility-tests.md
1.3 Find Reference Component
ls packages/bpk-component-*/
Phase 2: Component Creation (30-45 mins)
2.1 Create Package Structure
mkdir -p packages/bpk-component-[name]/src
mkdir -p examples/bpk-component-[name]
Required files:
packages/bpk-component-[name]/
├── README.md
├── index.ts
└── src/
├── Bpk[ComponentName].tsx
├── Bpk[ComponentName].module.scss
├── Bpk[ComponentName]-test.tsx
└── accessibility-test.tsx
examples/bpk-component-[name]/
├── examples.tsx
├── examples.module.scss
└── stories.tsx
2.2 Convert Component TypeScript
Key transformations:
import BpkText, { TEXT_STYLES } from '../../bpk-component-text';
import { cssModules, getDataComponentAttribute } from '../../bpk-react-utils';
import STYLES from './Bpk[ComponentName].module.scss';
const getClassName = cssModules(STYLES);
export type Bpk[ComponentName]Props = {
someRequiredProp: string;
someOptionalProp?: ThingType;
};
const Bpk[ComponentName] = ({
someOptionalProp = DEFAULT_VALUE,
someRequiredProp,
}: Bpk[ComponentName]Props) => (
<div
className={getClassName('bpk-[component-name]')}
{...getDataComponentAttribute('[ComponentName]')}
data-testid="bpk-[component-name]"
>
{/* Component content */}
</div>
);
export default Bpk[ComponentName];
Critical changes from external code:
- ✅ Add Apache 2.0 license header (NON-NEGOTIABLE)
- ✅ Use relative imports (
../../bpk-component-*)
- ✅ Remove product-specific dependencies (i18n, app utilities)
- ✅ Use
cssModules(STYLES) pattern, not custom CSS utility
- ✅ Add
data-testid for testing
- ✅ Add
{...getDataComponentAttribute('[ComponentName]')} to root element (component name WITHOUT "Bpk" prefix)
- ✅ Keep props interface minimal — only declare props the component genuinely needs
- ✅ Replace
div layout wrappers with BpkLayout components — use BpkHStack for rows, BpkVStack for columns, BpkBox for generic containers. Preserve gap/align/justify by mapping to gap={BpkSpacing.X}, alignItems, justifyContent props. Wrap any usage in <BpkProvider> (Chakra UI context requirement).
- ❌ NO raw
<div> for layout — always prefer BpkLayout components
- ❌ NO
className or style props for new components (API encapsulation, constitution XI)
- ❌ NO
& Omit<ComponentPropsWithoutRef<'div'>, 'children'> — do NOT spread HTML element props. Default to a closed, explicit props interface. Only use element spread when the component is explicitly a thin wrapper that must forward all native attributes (rare).
- ❌ NO product-specific i18n hooks
Why no HTML element spread?
Extending ComponentPropsWithoutRef<'div'> seems convenient but in practice:
- It exposes dozens of irrelevant props (
onPointerEnterCapture, aria-*, data-*, etc.)
- It lets consumers bypass intentional API constraints
- It makes the component contract unclear and harder to evolve
- It couples the component to a specific HTML element
Only use it when the component is explicitly a thin wrapper around a native element (e.g. a styled
<button> that must accept all button attributes).
2.3 Convert Styles (Modern Sass API)
@use '../../bpk-mixins/tokens';
@use '../../bpk-mixins/utils';
.bpk-[component-name] {
display: flex;
padding: tokens.bpk-spacing-base();
background-color: tokens.$bpk-surface-contrast-day;
color: tokens.$bpk-text-on-dark-day;
border-radius: tokens.$bpk-border-radius-lg;
gap: tokens.bpk-spacing-md();
@include utils.bpk-rtl {
}
@media (prefers-reduced-motion: reduce) {
animation: none;
}
&__element {
}
&--modifier {
}
}
@keyframes bpk-[component]-animation {
from { }
to { }
}
Critical patterns:
- ✅ Use
@use not @import (deprecated)
- ✅ Import specific mixins:
@use '../../bpk-mixins/tokens'
- ✅ Use token functions:
tokens.bpk-spacing-md()
- ✅ Use token variables:
tokens.$bpk-surface-contrast-day
- ✅ All sizing in
rem, never px (accessibility requirement)
- ✅ For any
rem value, first search @skyscanner/bpk-foundations-web tokens for a matching spacing/size token (e.g. tokens.bpk-spacing-xxl() = 2.5rem, tokens.bpk-spacing-xl() = 2rem). Only use raw rem if no token matches, in which case use tokens.$bpk-one-pixel-rem * <px-value> (e.g. tokens.$bpk-one-pixel-rem * 280 for 280px)
- ✅ BEM naming:
.bpk-[name], .bpk-[name]__[element], .bpk-[name]--[modifier]
- ✅ Support RTL with
@include utils.bpk-rtl
- ✅ Support reduced motion preference
- ❌ NO magic numbers - use design tokens
- ❌ NO inline colors like
#FFFFFF - use tokens.$bpk-color-white
2.4 Write Unit Tests
import { render, screen } from '@testing-library/react';
import Bpk[ComponentName] from './Bpk[ComponentName]';
describe('Bpk[ComponentName]', () => {
it('should render correctly with default props', () => {
const { asFragment } = render(
<Bpk[ComponentName] someRequiredProp="value" />,
);
expect(asFragment()).toMatchSnapshot();
});
it('should render provided content', () => {
render(<Bpk[ComponentName] someRequiredProp="value" />);
expect(screen.getByTestId('bpk-[component-name]')).toBeInTheDocument();
});
it('should render with optional prop', () => {
render(
<Bpk[ComponentName]
someRequiredProp="value"
someOptionalProp={SOME_VALUE}
/>,
);
expect(screen.getByTestId('bpk-[component-name]')).toBeInTheDocument();
});
});
2.5 Write Accessibility Tests
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import Bpk[ComponentName] from './Bpk[ComponentName]';
describe('Bpk[ComponentName] accessibility tests', () => {
it('should not have programmatically-detectable accessibility issues', async () => {
const { container } = render(
<Bpk[ComponentName] someRequiredProp="value" />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should not have accessibility issues with optional prop', async () => {
const { container } = render(
<Bpk[ComponentName]
someRequiredProp="value"
someOptionalProp={SOME_VALUE}
/>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
2.6 Create Storybook Integration
examples/bpk-component-[name]/examples.tsx:
import Bpk[ComponentName] from '../../packages/bpk-component-[name]/src/Bpk[ComponentName]';
import { cssModules } from '../../packages/bpk-react-utils';
import STYLES from './examples.module.scss';
const getClassName = cssModules(STYLES);
export const DefaultExample = () => (
<div className={getClassName('examples')}>
<Bpk[ComponentName] someRequiredProp="value" />
</div>
);
export const CustomExample = () => (
<div className={getClassName('examples')}>
<Bpk[ComponentName]
someRequiredProp="value"
someOptionalProp={SOME_VALUE}
/>
</div>
);
examples/bpk-component-[name]/stories.tsx:
import Bpk[ComponentName] from '../../packages/bpk-component-[name]/src/Bpk[ComponentName]';
import {
DefaultExample,
CustomExample,
} from './examples';
export default {
title: 'bpk-component-[name]',
component: Bpk[ComponentName],
};
export const Default = DefaultExample;
export const Custom = CustomExample;
export const VisualTest = DefaultExample;
export const VisualTestWithZoom = {
render: DefaultExample,
args: {
zoomEnabled: true,
},
};
2.7 Create Package Index
packages/bpk-component-[name]/index.ts:
import Bpk[ComponentName] from './src/Bpk[ComponentName]';
export type { Bpk[ComponentName]Props } from './src/Bpk[ComponentName]';
export default Bpk[ComponentName];
Pattern rationale: Matches the majority of Backpack components (button, card, chip, text). Always explicitly import the component, then export default — do not use the export { default } from shorthand.
2.8 Create README
Standard README format (follow button/chip pattern — keep it minimal):
# bpk-component-[name]
> Backpack [name] component.
## Installation
Check the main [Readme](https://github.com/skyscanner/backpack#usage) for a complete installation guide.
## Usage
\`\`\`tsx
import Bpk[ComponentName] from '@skyscanner/backpack-web/bpk-component-[name]';
export default () => (
<Bpk[ComponentName] someRequiredProp="value" />
);
\`\`\`
## Props
| Property | PropType | Required | Default Value |
| ---------------- | -------- | -------- | ------------- |
| someRequiredProp | string | true | - |
| someOptionalProp | string | false | 'default' |
What NOT to include in README (not in standard Backpack format):
- ❌ Tracking section (internal implementation detail)
- ❌ Accessibility section (covered by WCAG standards, not per-component docs)
- ❌ Design tokens section (implementation detail)
- ❌ Animation section (implementation detail)
- ❌ Features list (redundant with Usage examples)
Phase 3: Verification & Acceptance (15-20 mins)
CRITICAL: The component MUST pass the full test suite before being considered complete.
3.1 Full Test Suite (Acceptance Criteria)
Run the complete Backpack verification suite:
npm run lint && npm run check-react-versions && npm run check-bpk-dependencies && npm run jest
Success Criteria:
- ✅ Linting passes (0 errors, warnings in new component code are acceptable if justified)
- ✅ React version checks pass
- ✅ Backpack dependency checks pass
- ✅ All Jest tests pass with 100% coverage for the new component
- ✅ No TypeScript compilation errors
- ✅ Accessibility tests pass (jest-axe)
Note: Global coverage may be low when testing a single component, but the component itself must have 100% coverage:
packages/bpk-component-[name]/src | 100 | 100 | 100 | 100 |
3.2 Individual Verification Steps
If the full suite fails, debug with individual commands:
Type Check:
npm run typecheck
Lint (JS/TS):
npm run lint:js
Lint (SCSS):
npm run lint:scss
Component Tests Only:
npm run jest -- packages/bpk-component-[name]
3.3 Storybook Visual Verification
npm run storybook
Manual Checks:
3.4 Common Acceptance Failures
Failure: Lint errors in dist-sassdoc or generated files
Symptom: Lint fails with errors in dist-sassdoc/, dist/, or other generated directories
Solution: Ensure .eslintignore includes all generated directories:
node_modules
dist
dist-storybook
dist-sassdoc
coverage
Failure: Undefined Sass token errors
Symptom: Undefined variable: tokens.$bpk-border-radius-pill
Solution: Check token exists in @skyscanner/bpk-foundations-web:
- Search existing components for similar usage
- Use standard CSS values for unavailable tokens (e.g.,
50% for circles instead of $bpk-border-radius-pill)
- For pixel values, convert to rem:
10px → 0.625rem
Failure: Module resolution errors
Symptom: Module not found: Can't resolve '../../bpk-component-*'
Solution: Verify relative import paths:
- From component source:
../../bpk-component-text
- From examples:
../../packages/bpk-component-text
- From tests: Same as component source (tests are co-located)
Failure: Global coverage threshold not met
Symptom: Jest: "global" coverage threshold for statements (75%) not met: 17.22%
Expected: This is normal when testing a single component. Check that YOUR component has 100% coverage:
packages/bpk-component-[name]/src | 100 | 100 | 100 | 100 |
Bpk[ComponentName].tsx | 100 | 100 | 100 | 100 |
Failure: Snapshot mismatch
Symptom: 1 snapshot failed
Solution: For new components, this is expected on first run:
npm run jest -- packages/bpk-component-[name] -u
Common Issues & Solutions
Issue 1: Token Import Errors
Symptom: Error: Can't find module '@skyscanner/bpk-foundations-web'
Solution:
npm install
npm run build
Issue 2: Wrong Import Paths
Symptom: Module not found: Can't resolve '../../../bpk-component-text'
Solution: Use correct relative paths:
- From component:
../../bpk-component-text
- From examples:
../../packages/bpk-component-text
Issue 3: CSS Classes Not Applied
Symptom: Component renders but no styles visible
Solution: Check:
.module.scss extension used (not just .scss)
- Styles imported correctly:
import STYLES from './Component.module.scss'
cssModules(STYLES) pattern used correctly
- Class names follow BEM:
.bpk-[name]
Issue 4: Accessibility Test Failures
Symptom: jest-axe reports violations
Common fixes:
- Decorative elements have
aria-hidden="true"
- Interactive elements have proper
role attributes and labels
- Color contrast meets WCAG AA standards
- If visible text is the only identifier, no extra
aria-label is needed — screen readers will read the rendered text
Issue 5: Snapshot Mismatches
Symptom: Snapshot tests fail on first run
Solution: This is expected for new components:
npm test -- packages/bpk-component-[name] -u
Verification Checklist
MANDATORY ACCEPTANCE TEST (must pass before merge):
Code Quality:
Accessibility (WCAG 2.1 AA):
Testing:
Documentation & Examples:
RTL & Responsiveness:
Notes
Design Approval Required
CRITICAL: Before starting migration, component MUST have:
- Design approval from Backpack squad (#backpack Slack)
- Figma designs with all states documented
- Accessibility annotations in Figma
- Token specifications (no magic numbers in designs)
Do NOT proceed without design approval - this is non-negotiable per constitution.
When to Skip Migration
Don't migrate if:
- Component is too product-specific (not reusable)
- Component doesn't meet accessibility standards
- Design patterns don't align with Backpack philosophy
- Component has unstable API still under experimentation
Consider creating as experimental V2 component instead if uncertain.
API Encapsulation for New Components
Per constitution principle XI:
- NEW components must NOT expose
className or style props
- NEW components must NOT spread HTML element props via
& Omit<ComponentPropsWithoutRef<'div'>, 'children'> or similar — default to a closed, explicit props interface
- Only declare props the component actually uses
- Prevents consumers from bypassing the design system's visual constraints
- Makes the component contract clear and easy to evolve
- Existing components may grandfather
className for backward compatibility, but do NOT replicate this in new components
Dependency Removal
Common product-specific dependencies to remove:
@skyscanner-web/*/src/services/i18n → Use content prop instead
- Product-specific utility functions → Use Backpack utilities
- Custom CSS modules patterns → Use
cssModules(STYLES)
- App-specific types → Use standard React types
Performance Considerations
- Minimize bundle size - avoid unnecessary dependencies
- Use dynamic imports for large dependencies
- Follow
browserslist-config-skyscanner for transpilation
- No polyfills in component code (handled by app layer)
References