Skip to main content

migrate-component-to-scss-modules

Migrates an existing Emotion-styled React component in src/app/components to SCSS Modules. Use when asked to migrate, convert or batch-migrate components off Emotion (index.styles.ts / css prop) to index.module.scss, or when removing Emotion from a component.

소스 정보

저장소
bbc/simorgh
최근 소스 활동
2026년 9월 30일 06:34
감지된 SKILL.md 언어
영어
스타
1,718
포크
280

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
migrate-component-to-scss-modules
description
Migrates an existing Emotion-styled React component in src/app/components to SCSS Modules. Use when asked to migrate, convert or batch-migrate components off Emotion (index.styles.ts / css prop) to index.module.scss, or when removing Emotion from a component.
# Migrate Component to SCSS Modules Converts a component in `src/app/components/**` from Emotion (`index.styles.ts` + `css` prop) to SCSS Modules (`index.module.scss` + `className`). Styling *rules* live in `.github/instructions/styling-standards.instructions.md` and auto-apply to `src/app/components/**`. This skill covers the *mechanical conversion*: token mapping, workflow and verification. ## Step 1: Triage before starting Check the component against these exclusions and **stop and report** if any apply, rather than working around them: | Condition | Why it blocks | |---|---| | Imports `legacy/components/Grid` or `psammead-grid` | Layout still renders Emotion-styled legacy grid; needs a layout decision, not a styling change | | Passes a `css` prop into a legacy component | The legacy child consumes Emotion styles directly; `className` is not a drop-in replacement | | Imports a legacy `index.styles` file | Depends on a legacy Emotion style object that isn't yours to convert | | Uses a palette/spacing value as a runtime prop (not a style) | SCSS variables can't be passed as props; needs a design decision | | Component is outside `src/app/components` | Legacy/container/psammead components need modernisation first | Known blocked components: `Disclaimer`, `MostRead/Canonical/Item`, `MostRead/Canonical/List`, `MostRead/Label`, `RelatedTopics`, `Recommendations`, `RelatedContentSection`. Rendering a legacy component is **not** by itself a blocker — `ArticleLinksBlock` renders `SkipLinkWrapper` and migrated cleanly. It only blocks when the legacy child controls layout or receives styles. `getAmpLiteCss` inlines `.module.scss` chunks automatically, so AMP and Lite components need no special wiring. Minimize the compiled CSS for every edition; if the component renders in AMP, manually verify that the final inlined `style[amp-custom]` payload remains below AMP's hard 75 KB limit. Legacy imports that are **pure utilities** (e.g. `formatDuration` from `psammead-timestamp-container`) do not block migration. Before converting a shared component, search its consumers for Emotion `css` props such as `css={styles.inlineLink}`. An un-migrated consumer may intentionally replace the shared component's default styles; migrate that consumer in the same change or stop and report it as blocked. When a consumer owns a real DOM context for an override, express that relationship in SCSS rather than adding a wrapper solely to increase specificity. Use an ancestor selector (for example `.errorLinkWrapper .inlineLink`) or an element-qualified selector when the component fixes the element type (for example `p.copyright`). Exclude pseudo-states that remain owned by the shared component, such as `:visited`, `:hover`, and `:focus`. CSS Modules scopes class names but does not give a parent component automatic precedence. Use duplicated selectors only as a documented last resort when no meaningful context exists. ## Step 2: Map Emotion theme values to SCSS tokens All tokens are forwarded from `@scss/themeTokens`. Start every file with: ```scss @use '@scss/themeTokens' as theme; ``` ### Palette Emotion `palette.SCREAMING_SNAKE` → `theme.$palette-kebab-case`. | Emotion | SCSS | |---|---| | `palette.WHITE` | `theme.$palette-white` | | `palette.GREY_6` | `theme.$palette-grey-6` | | `palette.GREY_10` | `theme.$palette-grey-10` | | `palette.SHADOW` | `theme.$palette-shadow` | | `palette.POSTBOX_30` | `theme.$palette-postbox-30` | | `palette.MIDNIGHT_BLACK` | `theme.$palette-midnight-black` | ### Spacings Emotion values are unitless numbers needing `rem`; SCSS values already include units, so interpolate directly. | Emotion | SCSS | |---|---| | `spacings.HALF` | `theme.$spacings-half` | | `spacings.FULL` | `theme.$spacings-full` | | `spacings.DOUBLE` | `theme.$spacings-double` | | `spacings.TRIPLE` | `theme.$spacings-triple` | | `spacings.QUADRUPLE` | `theme.$spacings-quadruple` | | `spacings.QUINTUPLE` | `theme.$spacings-quintuple` | | `spacings.SEXTUPLE` | `theme.$spacings-sextuple` | ```scss padding: #{theme.$spacings-double}; ``` ### Media queries Emotion values include the `@media` prefix; SCSS variables contain only the condition, so they must be interpolated after `@media`. | Emotion | SCSS | |---|---| | `mq.GROUP_1_MIN_WIDTH` | `theme.$mediaQueries-group-1-min-width` | | `mq.GROUP_2_MIN_WIDTH` | `theme.$mediaQueries-group-2-min-width` | | `mq.GROUP_3_MIN_WIDTH` | `theme.$mediaQueries-group-3-min-width` | | `mq.GROUP_4_MIN_WIDTH` | `theme.$mediaQueries-group-4-min-width` | | `mq.GROUP_5_MIN_WIDTH` | `theme.$mediaQueries-group-5-min-width` | | `mq.GROUP_3_MAX_WIDTH` | `theme.$mediaQueries-group-3-max-width` | | `mq.FORCED_COLOURS` | `theme.$mediaQueries-forced-colours` | | `mq.POINTER` | `theme.$mediaQueries-pointer-fine` | ```scss @media #{theme.$mediaQueries-group-3-min-width} { ... } ``` `GROUP_N_ONLY` and `GROUP_1_AND_GROUP_2` have no direct equivalent — compose min and max variables in a single query. ### Typography When the Emotion style sets both a font size and variant, use the combined mixin: ```scss @include theme.typography-from-scale-and-variant(<scale>, '<variant>'); ``` The scale is unquoted and the variant is quoted kebab-case. The mixin emits the font family, style and weight together with responsive font size and line height. Remove any manual `line-height` the Emotion styles set alongside the GEL scale. Use the lower-level `theme.fontSizes-gel-font-size(<scale>)` or `theme.fontVariants-gel-font-variant('<variant>')` mixin only when the existing component intentionally sets one without the other. #### Runtime typography (size/variant known only from props) Sass mixin arguments are resolved at compile time, so `typography-from-scale-and-variant` cannot consume values supplied by component props at runtime. When `size` and/or `fontVariant` are component props, use the paired runtime API instead: ```tsx import { getTypographyCustomProperties } from '#app/components/ThemeProviderSCSSModules/typography'; <a className={styles.link} style={getTypographyCustomProperties({ size, fontVariant })} />; ``` ```scss .link { @include theme.typography-from-custom-properties; } ``` `getTypographyCustomProperties` writes inline `--gel-font-family`, `--gel-font-style`, `--gel-font-weight`, `--gel-font-size-group-{a,b,d}` and `--gel-line-height-group-{a,b,d}` custom properties from the runtime values; `typography-from-custom-properties` reads them back with `inherit` fallbacks. These runtime channel names are deliberately distinct from the static, variant-keyed theme tokens the compile-known mixin resolves directly (`--gel-font-variant-<variant>-font-family`, `--font-size-<scale>-group-{a,b,d}`) — see [typography.test.ts](../../../src/app/components/ThemeProviderSCSSModules/typography.test.ts) for the full static-vs-runtime contract. Valid scales: `atlas`, `elephant`, `imperial`, `royal`, `foolscap`, `canon`, `trafalgar`, `paragon`, `doublePica`, `greatPrimer`, `bodyCopy`, `pica`, `longPrimer`, `brevier`, `minion`. | Emotion | SCSS argument | |---|---| | `sansRegular` | `'sans-regular'` | | `sansRegularItalic` | `'sans-regular-italic'` | | `sansBold` | `'sans-bold'` | | `sansBoldItalic` | `'sans-bold-italic'` | | `sansLight` | `'sans-light'` | | `serifRegular` | `'serif-regular'` | | `serifMedium` | `'serif-medium'` | | `serifMediumItalic` | `'serif-medium-italic'` | | `serifBold` | `'serif-bold'` | | `serifLight` | `'serif-light'` | Font fallback resolution belongs to the theme layer. `ThemeProviderSCSSModules` font variant files expose the raw variant variables and the shared `fontVariantAliases.scss` file builds the fallback aliases consumed by the component mixins. Do not copy fallback chains into a component helper or its component tests; test changes to those chains at the theme/Sass layer. ### Pixel conversion `pixelsToRem(3)` → `theme.pixelsToRem-px-to-rem(3)` (returns a value with `rem` units already applied). ### Grid widths `gridWidths.scss` is forwarded as `theme.$gridWidths-*` (e.g. `theme.$gridWidths-grid-width-600`), already in `rem` — use it directly, no conversion needed. ## Step 3: Convert conditional logic out of JavaScript | Emotion pattern | SCSS Module replacement | |---|---| | `css={[styles.base, cond && styles.modifier]}` | `clsx(styles.base, cond && styles.modifier)` | | Theme callback reading dark UI state | `:global([data-is-dark-ui='true']) &` | | `dir === 'rtl' ? ... : ...` | Logical properties (`padding-inline-start`) | | Opera Mini branch | `:global(.is-opera-mini) &` | | Per-instance numeric value | Inline CSS custom property + `var()` | | Small set of named style variants | `data-*` attribute + `&[data-x='value']` selector | | Large set of possible values (e.g. a GEL scale) | Typed helper + inline CSS custom properties consumed by one SCSS rule set | ### Conditional style arrays The most common pattern in this codebase. Emotion's array syntax becomes composed class names. The condition is a **prop**, not the style object — `styles.alignWithMargin` always exists and is always truthy. Most components name the two differently (`isActive && styles.tabActive`); `CallToActionLink` is the exception and is easy to misread as a single reference. **Before:** ```tsx <a css={[styles.link, alignWithMargin && styles.alignWithMargin]}> ``` **After:** ```tsx import clsx from 'clsx'; <a className={clsx(styles.link, alignWithMargin && styles.alignWithMargin)}> ``` Emotion drops falsy entries from a `css` array automatically; `className` does not. `clsx` restores that behaviour, so this is a direct translation rather than a rewrite — a plain template literal would carry `&&` across as the literal string `false`: ```tsx // ❌ class="link_x1 false" when alignWithMargin is false <a className={`${styles.link} ${alignWithMargin && styles.alignWithMargin}`}> ``` Use `clsx` for any conditional combination of classes. Keep both classes as separate rules in the SCSS file rather than merging them into one. This works when the second class adds **different** properties to the base. If the variant instead **replaces** the same properties (a different font size, say), apply one class or the other rather than both — two classes setting identical properties makes the result depend on their order in the compiled CSS: ```tsx <h2 className={isLarge ? styles.titleLarge : styles.title}> ``` A **discrete variant** (boolean or small union) can legitimately select or add a class. What the styling standards prohibit is deriving a class from a **continuous or computed value**, or from `dir` — use a CSS custom property or logical properties for those. ### Props with many possible values When a prop has many possible values, avoid creating a separate class and CSS rule for every value unless the list is small. The browser downloads all of those rules, even though each instance uses only one value. For a small list, keep the value in a `data-*` attribute and select the matching rule in SCSS: ```tsx <a className={styles.link} data-size={size} /> ``` ```scss .link[data-size='small'] { font-size: 1rem; } ``` For a larger list, use one set of responsive rules and pass the selected values as CSS custom properties: ```tsx import type { CSSProperties } from 'react'; type LinkSize = 'small' | 'large'; type LinkStyles = CSSProperties & { '--link-font-size': string; '--link-line-height': string; }; const getStylesForSize = (size: LinkSize): LinkStyles => ({ '--link-font-size': size === 'small' ? '1rem' : '1.25rem', '--link-line-height': size === 'small' ? '1.5' : '1.25', }); <a className={styles.link} style={getStylesForSize(size)} /> ``` ```scss .link { font-size: var(--link-font-size, inherit); line-height: var(--link-line-height, inherit); } ``` Keep the code that maps prop values to custom-property values in one typed helper or at the component boundary. Do not repeat that mapping in both TypeScript and SCSS. See [src/app/components/InlineLink/index.module.scss](../../../src/app/components/InlineLink/index.module.scss) for a concrete compact CSS custom-property bridge: `size` and `fontVariant` are mapped by [typography.ts](../../../src/app/components/ThemeProviderSCSSModules/typography.ts) to inline `--gel-*` values, which one responsive rule set consumes via `theme.typography-from-custom-properties`. Keep the CSS small for every edition. If the component renders in AMP, measure the final inlined `style[amp-custom]` payload and keep the total below AMP's hard 75 KB limit. This is an additional AMP requirement; the CSS-minimization guidance applies to all editions. ### Consumer-owned style overrides When a migrated component needs a consumer-specific default style, keep the shared component's state rules and scope the override to the consumer's DOM context: ```scss .errorLinkWrapper { .inlineLink:not(:visited):not(:hover):not(:focus) { color: theme.$palette-black; border-bottom: #{theme.pixelsToRem-px-to-rem(1)} solid theme.$palette-black; } } ``` Do not assume the order of classes in the HTML or the React parent-child relationship controls the CSS cascade. Do not add a wrapper only to manufacture a selector context. Custom properties are appropriate for genuinely configurable component values, but adding a new override variable for every consumer couples the shared component to its consumers. ## Step 4: Apply the change
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기