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-02 changelog: | v1.4.0 (2026-03-02):
- Fixed index.ts template: use explicit
import ... + export defaultinstead ofexport { 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_backpackin 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.mdandAGENTS.mdfor project contextconstitution.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
# Get directory structure
gh api "repos/Skyscanner/[repo-name]/contents/[path-to-component]" \
--jq '.[] | {name: .name, type: .type, path: .path}'
# Download all component files
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
# Read constitution for requirements
cat .specify/memory/constitution.md
# Review relevant architectural decisions
ls decisions/
cat decisions/modern-sass-api.md
cat decisions/component-scss-filenames.md
cat decisions/accessibility-tests.md
1.3 Find Reference Component
# Look for similar existing Backpack component
ls packages/bpk-component-*/
# Study the reference component structure
# Good references: bpk-component-chip, bpk-component-button
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:
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2016 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 = {
// Only the props this component actually needs — no HTML element spread
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-testidfor testing - ✅ Add
{...getDataComponentAttribute('[ComponentName]')}to root element (component name WITHOUT "Bpk" prefix) - ✅ Keep props interface minimal — only declare props the component genuinely needs
- ✅ Replace
divlayout wrappers with BpkLayout components — useBpkHStackfor rows,BpkVStackfor columns,BpkBoxfor generic containers. Preserve gap/align/justify by mapping togap={BpkSpacing.X},alignItems,justifyContentprops. Wrap any usage in<BpkProvider>(Chakra UI context requirement). - ❌ NO raw
<div>for layout — always prefer BpkLayout components - ❌ NO
classNameorstyleprops 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)
/*
* Backpack - Skyscanner's Design System
* [... full license header ...]
*/
@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();
// RTL support
@include utils.bpk-rtl {
// RTL-specific overrides
}
// Reduced motion support
@media (prefers-reduced-motion: reduce) {
animation: none;
}
&__element {
// BEM naming: .bpk-[component]__[element]
}
&--modifier {
// BEM naming: .bpk-[component]--[modifier]
}
}
@keyframes bpk-[component]-animation {
from { /* ... */ }
to { /* ... */ }
}
Critical patterns:
- ✅ Use
@usenot@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, neverpx(accessibility requirement) - ✅ For any
remvalue, first search@skyscanner/bpk-foundations-webtokens for a matching spacing/size token (e.g.tokens.bpk-spacing-xxl()= 2.5rem,tokens.bpk-spacing-xl()= 2rem). Only use rawremif no token matches, in which case usetokens.$bpk-one-pixel-rem * <px-value>(e.g.tokens.$bpk-one-pixel-rem * 280for 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- usetokens.$bpk-color-white
2.4 Write Unit Tests
/*
* Backpack - Skyscanner's Design System
* [... license header ...]
*/
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
/*
* Backpack - Skyscanner's Design System
* [... license header ...]
*/
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:
/*
* Backpack - Skyscanner's Design System
* [... license header ...]
*/
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:
/*
* Backpack - Skyscanner's Design System
* [... license header ...]
*/
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:
/*
* Backpack - Skyscanner's Design System
* [... license header ...]
*/
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
# Should compile without errors or warnings
Lint (JS/TS):
npm run lint:js
# 0 errors in new component files
Lint (SCSS):
npm run lint:scss
# 0 errors in new component styles
Component Tests Only:
npm run jest -- packages/bpk-component-[name]
# All tests pass, 100% component coverage
3.3 Storybook Visual Verification
npm run storybook
Manual Checks:
- Component renders correctly in all examples
- Animations work smoothly
- Component is responsive at different viewport sizes
- RTL mode works correctly (if applicable)
- No console errors or warnings
- Reduced motion preference is respected
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
# Updates snapshots, then re-run to verify they pass
Common Issues & Solutions
Issue 1: Token Import Errors
Symptom: Error: Can't find module '@skyscanner/bpk-foundations-web'
Solution:
npm install
npm run build # Rebuild bpk-mixins package
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.scssextension 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
roleattributes and labels - Color contrast meets WCAG AA standards
- If visible text is the only identifier, no extra
aria-labelis 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
# Updates snapshots for new component
Verification Checklist
MANDATORY ACCEPTANCE TEST (must pass before merge):
- Full test suite passes:
npm run lint && npm run check-react-versions && npm run check-bpk-dependencies && npm run jest- 0 lint errors (warnings acceptable if justified)
- All React version checks pass
- All dependency checks pass
- All Jest tests pass
- Component has 100% code coverage
Code Quality:
- All files have Apache 2.0 license headers (NON-NEGOTIABLE)
- Root element has
{...getDataComponentAttribute('[ComponentName]')}(name WITHOUT "Bpk" prefix) - Component follows naming convention:
Bpk[ComponentName] - File names match component name exactly
- Modern Sass API used (
@use, not@import) - All spacing/colors use design tokens, no magic numbers
- Sizing in
remunits, notpx(accessibility requirement) - TypeScript compiles without errors or warnings
- No product-specific dependencies remain
Accessibility (WCAG 2.1 AA):
- Accessibility tests pass (jest-axe with 0 violations)
- Decorative elements have
aria-hidden="true" - Interactive elements have proper roles and labels
- Component respects
prefers-reduced-motion - Color contrast meets WCAG AA standards
Testing:
- Unit tests pass with 100% component coverage
- Tests cover all props and states
- Snapshot tests generated and passing
- Edge cases tested (long content, empty state, etc.)
Documentation & Examples:
- README documentation complete with usage examples
- Storybook examples render correctly
- All component states visible in Storybook
- Props table documented with types and defaults
RTL & Responsiveness:
- Component handles RTL languages (uses
@include utils.bpk-rtl) - Component is responsive at different viewport sizes
- Component works on mobile and desktop
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
classNameorstyleprops - 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
classNamefor backward compatibility, but do NOT replicate this in new components
Dependency Removal
Common product-specific dependencies to remove:
@skyscanner-web/*/src/services/i18n→ Usecontentprop 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-skyscannerfor transpilation - No polyfills in component code (handled by app layer)