From c2d5e27b194a02e18c3c0c5fcb6a6ad579927a43 Mon Sep 17 00:00:00 2001 From: Jon Rohan Date: Mon, 23 Mar 2026 13:29:48 -0700 Subject: [PATCH 1/3] Add feature-flags Copilot skill Adds a SKILL.md that provides agents with context on how feature flags work in Primer React, including architecture, usage patterns, testing, Storybook integration, and the feature flag lifecycle. --- .github/skills/feature-flags/SKILL.md | 211 ++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 .github/skills/feature-flags/SKILL.md diff --git a/.github/skills/feature-flags/SKILL.md b/.github/skills/feature-flags/SKILL.md new file mode 100644 index 00000000000..938da17127c --- /dev/null +++ b/.github/skills/feature-flags/SKILL.md @@ -0,0 +1,211 @@ +--- +name: feature-flags +description: 'Use when: implementing, creating, testing, or debugging feature flags in Primer React. Covers useFeatureFlag hook, FeatureFlags provider, DefaultFeatureFlags, FeatureFlagScope, Storybook integration, and the feature flag lifecycle at GitHub.' +--- + +# Feature Flags in Primer React + +Feature flags provide a way to incrementally build and deliver changes in Primer alongside the feature flag system at GitHub. They help build confidence in changes and improve the reliability of releases. + +## Architecture + +Feature flags are implemented in `packages/react/src/FeatureFlags/` with these core pieces: + +| File | Purpose | +| ------------------------ | ----------------------------------------------- | +| `FeatureFlags.tsx` | React context provider component | +| `useFeatureFlag.ts` | Hook to check if a flag is enabled | +| `FeatureFlagScope.ts` | Class that manages flag collections and merging | +| `DefaultFeatureFlags.ts` | Default flag values (all flags listed here) | +| `FeatureFlagContext.ts` | React context definition | +| `index.ts` | Public exports | + +### Exports + +Feature flags are exported from `@primer/react/experimental`: + +```tsx +import {FeatureFlags, useFeatureFlag, DefaultFeatureFlags} from '@primer/react/experimental' +``` + +They are NOT exported from the main `@primer/react` entry point. + +## How to Use a Feature Flag in a Component + +### 1. Add the flag to DefaultFeatureFlags + +Register your flag in `packages/react/src/FeatureFlags/DefaultFeatureFlags.ts` with a default value of `false`: + +```typescript +export const DefaultFeatureFlags = FeatureFlagScope.create({ + // ...existing flags... + primer_react_my_new_feature: false, +}) +``` + +### 2. Use `useFeatureFlag` in your component + +```tsx +import {useFeatureFlag} from '../FeatureFlags' + +function MyComponent() { + const enabled = useFeatureFlag('primer_react_my_new_feature') + + if (enabled) { + return + } + return +} +``` + +### 3. Naming conventions + +- Prefix with `primer_react_` for flags in the `@primer/react` package +- Use snake_case: `primer_react_my_feature_name` + +## Patterns + +### Change behavior conditionally + +```tsx +function ExampleComponent({children}) { + const enabled = useFeatureFlag('primer_react_my_feature') + + return ( + + ) +} +``` + +### Toggle between two component versions + +```tsx +function ExampleComponent(props) { + const enabled = useFeatureFlag('primer_react_my_feature') + if (enabled) { + return + } + return +} +``` + +### Data attributes on DOM elements + +```tsx +function MyOverlay() { + const enabled = useFeatureFlag('primer_react_my_feature') + return
+} +``` + +## Real Codebase Examples + +### Spinner animation sync (`packages/react/src/Spinner/Spinner.tsx`) + +```tsx +const syncAnimationsEnabled = useFeatureFlag('primer_react_spinner_synchronize_animations') +const shouldSync = syncAnimationsEnabled && noMotionPreference +const mergedStyle = shouldSync ? {...style, animationDelay: `${syncDelay}ms`} : style +``` + +### CSS anchor positioning (`packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx`) + +```tsx +const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning') +useEffect(() => { + if (cssAnchorPositioning && !hasLoadedAnchorPositioningPolyfill.current) { + applyAnchorPositioningPolyfill() + hasLoadedAnchorPositioningPolyfill.current = true + } +}, [open, overlayRef, updateOverlayRef, cssAnchorPositioning]) +``` + +### Breadcrumbs overflow (`packages/react/src/Breadcrumbs/Breadcrumbs.tsx`) + +```tsx +const overflowMenuEnabled = useFeatureFlag('primer_react_breadcrumbs_overflow_menu') +``` + +## Testing with Feature Flags + +Wrap your component with the `FeatureFlags` provider to set flag values in tests: + +```tsx +import {FeatureFlags} from '../../FeatureFlags' + +// Test with flag enabled +render( + + + , +) + +// Test with flag disabled (or omit the wrapper entirely — flags default to false) +render( + + + , +) +``` + +### Key testing behaviors + +- Unknown flags default to `false` +- Nested `FeatureFlags` providers merge flags — inner values override outer values +- You should test both enabled and disabled states + +## Storybook Integration + +### Enable flag in a specific story + +```tsx +import {FeatureFlags} from '../../FeatureFlags' + +export const WithFeatureEnabled = () => ( + + + +) + +export const WithFeatureDisabled = () => +``` + +### Enable flag in all stories + +Add your flag to the `FeatureFlags` wrapper in `packages/react/.storybook/preview.jsx`. The preview already wraps all stories in a `FeatureFlags` provider using values from `DefaultFeatureFlags`. + +To enable a flag globally via environment, add it to the `featureFlagEnvList` set in `preview.jsx` and set `VITE_=1`. + +## Feature Flag Lifecycle + +1. **Create** — Register the flag in `DefaultFeatureFlags.ts` with value `false` +2. **Implement** — Use `useFeatureFlag()` in components, write tests for both states +3. **Test in Storybook** — Create stories with the flag enabled/disabled +4. **Ship** — Merge to main; the flag remains off by default +5. **Enable at GitHub** — Use DevPortal to enable for specific actors, then staffship, then GA +6. **Remove** — Once fully rolled out, remove the flag from code and `DefaultFeatureFlags.ts` + +### Temporarily testing in Dotcom CI + +Set your flag to `true` in `DefaultFeatureFlags.ts` to enable it by default for CI testing. Set it back to `false` before merging. + +## FeatureFlags Provider Internals + +- `FeatureFlagScope.create(flags)` — Creates a scope from a plain object `{[key: string]: boolean}` +- `FeatureFlagScope.merge(a, b)` — Merges two scopes; `b` values override `a` +- The `FeatureFlags` component merges parent context flags with the provided `flags` prop +- `DefaultFeatureFlags` is the initial context value — it defines all known flags + +## Current Feature Flags + +Check `packages/react/src/FeatureFlags/DefaultFeatureFlags.ts` for the current list of all registered feature flags and their default values. From b5ffa255502ee0ac6278b6e7700cb539000a3b06 Mon Sep 17 00:00:00 2001 From: Jon Rohan Date: Mon, 23 Mar 2026 13:38:38 -0700 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/skills/feature-flags/SKILL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/skills/feature-flags/SKILL.md b/.github/skills/feature-flags/SKILL.md index 938da17127c..07c64a40154 100644 --- a/.github/skills/feature-flags/SKILL.md +++ b/.github/skills/feature-flags/SKILL.md @@ -104,7 +104,7 @@ function ExampleComponent(props) { ```tsx function MyOverlay() { const enabled = useFeatureFlag('primer_react_my_feature') - return
+ return
} ``` @@ -150,7 +150,7 @@ render( , ) -// Test with flag disabled (or omit the wrapper entirely — flags default to false) +// Test with flag disabled (or omit the wrapper to use default values from DefaultFeatureFlags) render( @@ -182,9 +182,9 @@ export const WithFeatureDisabled = () => ### Enable flag in all stories -Add your flag to the `FeatureFlags` wrapper in `packages/react/.storybook/preview.jsx`. The preview already wraps all stories in a `FeatureFlags` provider using values from `DefaultFeatureFlags`. +Storybook's global preview (`packages/react/.storybook/preview.jsx`) already wraps all stories in a `FeatureFlags` provider, using `DefaultFeatureFlags` as the source of default values and toolbar options. In most cases you only need to register your flag in `DefaultFeatureFlags.ts`; you do not need to manually add it to the `FeatureFlags` wrapper. -To enable a flag globally via environment, add it to the `featureFlagEnvList` set in `preview.jsx` and set `VITE_=1`. +To enable a flag globally via environment, add its exact flag name (for example, `primer_react_my_feature`) to the `featureFlagEnvList` set in `preview.jsx`, and set the corresponding env var to `1` (for example, `VITE_primer_react_my_feature=1`). The preview code reads `import.meta.env[\`VITE_${flag}\`]`, so the part after `VITE_` must match the flag string exactly. ## Feature Flag Lifecycle From c4fa13d53e0c1c916216206a5509fc4b113713ec Mon Sep 17 00:00:00 2001 From: jonrohan <54012+jonrohan@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:44:58 +0000 Subject: [PATCH 3/3] chore: auto-fix lint and formatting issues --- .github/skills/feature-flags/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/skills/feature-flags/SKILL.md b/.github/skills/feature-flags/SKILL.md index 07c64a40154..f4e442a3618 100644 --- a/.github/skills/feature-flags/SKILL.md +++ b/.github/skills/feature-flags/SKILL.md @@ -184,7 +184,7 @@ export const WithFeatureDisabled = () => Storybook's global preview (`packages/react/.storybook/preview.jsx`) already wraps all stories in a `FeatureFlags` provider, using `DefaultFeatureFlags` as the source of default values and toolbar options. In most cases you only need to register your flag in `DefaultFeatureFlags.ts`; you do not need to manually add it to the `FeatureFlags` wrapper. -To enable a flag globally via environment, add its exact flag name (for example, `primer_react_my_feature`) to the `featureFlagEnvList` set in `preview.jsx`, and set the corresponding env var to `1` (for example, `VITE_primer_react_my_feature=1`). The preview code reads `import.meta.env[\`VITE_${flag}\`]`, so the part after `VITE_` must match the flag string exactly. +To enable a flag globally via environment, add its exact flag name (for example, `primer_react_my_feature`) to the `featureFlagEnvList` set in `preview.jsx`, and set the corresponding env var to `1` (for example, `VITE_primer_react_my_feature=1`). The preview code reads `import.meta.env[\`VITE*${flag}\`]`, so the part after `VITE*` must match the flag string exactly. ## Feature Flag Lifecycle