# Backpack External Component Migration

> 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 (pnpm run lint && pnpm run check-react-versions && pnpm run check-bpk-dependencies && pnpm run jest) with 0 errors before acceptance.

- Skill: `skyscanner/backpack-external-component-migration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add skyscanner/backpack-external-component-migration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/skyscanner/backpack-external-component-migration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: skyscanner (https://skillmd.com/u/skyscanner)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/skyscanner/backpack-external-component-migration

---


# 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

1. **GitHub CLI access** configured (`gh auth status`)
2. **Backpack repository** cloned locally
3. **Repository documentation access**:
   - `CLAUDE.md` and `AGENTS.md` for project context
   - `constitution.md` (in `.specify/memory/`)
   - `decisions/` directory for architectural decisions
4. **Design approval** from Backpack squad (required before starting)

## Solution

### Phase 1: Discovery & Research (15-20 mins)

#### 1.1 Extract External Component

```bash
# 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

```bash
# 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

```bash
# Look for similar existing Backpack component
ls packages/backpack-web/src/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

```bash
mkdir -p packages/backpack-web/src/bpk-component-[name]/src
mkdir -p examples/bpk-component-[name]
```

**Required files:**
```
packages/backpack-web/src/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:**

```typescript
/*
 * 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-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)

```scss
/*
 * 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 `@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

```typescript
/*
 * 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

```typescript
/*
 * 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:**
```typescript
/*
 * Backpack - Skyscanner's Design System
 * [... license header ...]
 */

import Bpk[ComponentName] from '../../packages/backpack-web/src/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:**
```typescript
/*
 * Backpack - Skyscanner's Design System
 * [... license header ...]
 */

import Bpk[ComponentName] from '../../packages/backpack-web/src/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/backpack-web/src/bpk-component-[name]/index.ts:**
```typescript
/*
 * 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):

```markdown
# 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:

```bash
pnpm run lint && pnpm run check-react-versions && pnpm run check-bpk-dependencies && pnpm 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/backpack-web/src/bpk-component-[name]/src  | 100 | 100 | 100 | 100 |
```

#### 3.2 Individual Verification Steps

If the full suite fails, debug with individual commands:

**Type Check:**
```bash
pnpm run typecheck
# Should compile without errors or warnings
```

**Lint (JS/TS):**
```bash
pnpm run lint:js
# 0 errors in new component files
```

**Lint (SCSS):**
```bash
pnpm run lint:scss
# 0 errors in new component styles
```

**Component Tests Only:**
```bash
pnpm run jest -- packages/backpack-web/src/bpk-component-[name]
# All tests pass, 100% component coverage
```

#### 3.3 Storybook Visual Verification

```bash
pnpm 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`:
1. Search existing components for similar usage
2. Use standard CSS values for unavailable tokens (e.g., `50%` for circles instead of `$bpk-border-radius-pill`)
3. 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/backpack-web/src/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/backpack-web/src/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:
```bash
pnpm run jest -- packages/backpack-web/src/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:**
```bash
pnpm install
pnpm 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/backpack-web/src/bpk-component-text`

### Issue 3: CSS Classes Not Applied

**Symptom:** Component renders but no styles visible

**Solution:** Check:
1. `.module.scss` extension used (not just `.scss`)
2. Styles imported correctly: `import STYLES from './Component.module.scss'`
3. `cssModules(STYLES)` pattern used correctly
4. 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:
```bash
pnpm test -- packages/backpack-web/src/bpk-component-[name] -u
# Updates snapshots for new component
```

## Verification Checklist

**MANDATORY ACCEPTANCE TEST** (must pass before merge):
- [ ] **Full test suite passes**: `pnpm run lint && pnpm run check-react-versions && pnpm run check-bpk-dependencies && pnpm 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 `rem` units, not `px` (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 `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

- [Backpack Constitution](../../.specify/memory/constitution.md)
- [Backpack Documentation](https://www.skyscanner.design/)
- [Modern Sass API Decision](../../decisions/modern-sass-api.md)
- [Accessibility Testing Decision](../../decisions/accessibility-tests.md)
- [Component SCSS Filenames Decision](../../decisions/component-scss-filenames.md)
- [Sizing in Rem Decision](../../decisions/sizing-in-rem.md)
- [GitHub CLI Authentication](https://cli.github.com/manual/gh_auth_login)

