Skip to content

chore: improve tailwind intellisense and linting for react native#783

Merged
georgewrmarshall merged 8 commits intomainfrom
cursor/generate-tailwind-config-for-design-system-9a8d
Jul 16, 2025
Merged

chore: improve tailwind intellisense and linting for react native#783
georgewrmarshall merged 8 commits intomainfrom
cursor/generate-tailwind-config-for-design-system-9a8d

Conversation

@georgewrmarshall
Copy link
Contributor

@georgewrmarshall georgewrmarshall commented Jul 16, 2025

Description

This PR significantly enhances the React Native developer experience by adding comprehensive Tailwind IntelliSense and ESLint support to the @metamask/design-system-twrnc-preset package. This bridges the gap between TWRNC usage and standard Tailwind tooling, providing React Native developers with the same excellent developer experience as React Web developers.

What is the reason for the change?
React Native developers using TWRNC were missing critical developer experience features:

  • No IntelliSense/autocomplete for design system classes
  • No ESLint validation of Tailwind classnames
  • No detection of invalid classes, ordering issues, or shorthand suggestions
  • Difficult discovery of available design system tokens
  • Manual enforcement of design system color usage

What is the improvement/solution?
We've implemented a comprehensive Tailwind configuration system that:

  • Generates static tailwind.config.js files compatible with standard Tailwind tooling
  • Provides full IntelliSense support with autocomplete for all design system classes
  • Enables ESLint integration with eslint-plugin-tailwindcss for React Native projects
  • Enforces design system token usage (eliminates default Tailwind colors like red-500)
  • Supports both template literals (tw\classnames`) and function calls (tw('classnames')`)
  • Maintains theme-agnostic classnames that work with both light and dark themes

Related issues

Fixes: #747

Manual testing steps

  1. IntelliSense Testing:

    • Open a React Native file in VS Code (e.g., packages/design-system-react-native/src/components/Button/Button.tsx)
    • Start typing tw\bg-and verify IntelliSense shows design system colors likebg-default, bg-primary-default`
    • Try typing tw\text-` and verify design system text colors appear
    • Confirm no default Tailwind colors like bg-red-500 appear in suggestions
  2. ESLint Validation Testing:

    • Try typing an invalid class like tw\bg-red-500` and verify ESLint flags it as invalid
    • Test class ordering suggestions (e.g., tw\p-4 flexsuggests reordering totw`flex p-4`)
    • Verify shorthand suggestions work (e.g., tw\h-4 w-4suggeststw`size-4`)
  3. Build and Test:

    • Run yarn workspace @metamask/design-system-react-native test - should pass
    • Run yarn lint - should pass with only expected template literal warnings
    • Run yarn workspace @metamask/design-system-twrnc-preset build - should build successfully

Screenshots/Recordings

Before

  • No IntelliSense for design system classes in React Native
  • No ESLint validation of Tailwind classnames
  • Manual discovery of available design tokens
  • Potential usage of non-design-system colors
  • String concatenation patterns with .trim()

After

  • Full IntelliSense with autocomplete for all design system classes
  • ESLint catches invalid classnames and ordering issues
  • Automatic enforcement of design system token usage
  • Direct tw template literal usage for better tooling support
  • Improved developer productivity with instant feedback
intellisense720.mov

Mobile storybook and components still render as expected

after720.mov

Comment on lines +44 to +45
const expectedTextColor = tw`text-muted`.color;
const expectedFontSize = tw`text-body-sm`.fontSize;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing invalid classname linting violation

const tw = useTailwind();
// Compute expected container style using an empty twClassName.
const computedExpectedContainer = tw`h-[16px] w-[16px] items-center justify-center rounded-full bg-icon-default`;
const computedExpectedContainer = tw`size-[16px] items-center justify-center rounded-full bg-icon-default`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint fix

const box = getByTestId('box');
const styles = flattenStyles(box.props.style);
expect(styles[0]).toStrictEqual(tw`bg-primary-default flex p-4`);
expect(styles[0]).toStrictEqual(tw`flex bg-primary-default p-4`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint fix


return (
<View style={[tw`${twContainerClassNames}`, style]} {...props}>
<View style={[twContainerClassNames, style]} {...props}>
Copy link
Contributor Author

@georgewrmarshall georgewrmarshall Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should try to avoid this way of defining tailwind classnames in react native because unless the classnames appear in tw tailwind intellisense and the eslint tailwind plugin won't work. We should likely refactor all of our react-native components to fix this

const { result } = renderHook(() => useTailwind());
const tw = result.current;
const expected = tw`items-center justify-center ${TWCLASSMAP_BUTTONICON_SIZE_DIMENSION[ButtonIconSize.Md]} text-primary-default rounded-sm bg-transparent opacity-100`;
const expected = tw`items-center justify-center ${TWCLASSMAP_BUTTONICON_SIZE_DIMENSION[ButtonIconSize.Md]} rounded-sm bg-transparent text-primary-default opacity-100`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lint fix

expect.arrayContaining([
expect.objectContaining(
tw`flex h-[22px] w-[22px] items-center justify-center rounded border-2 border-error-default bg-default`,
tw`flex size-[22px] items-center justify-center rounded border-2 border-error-default bg-default`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint fix

{...checkboxContainerProps}
style={[
tw`${getCheckboxContainerStyle(pressed)} flex h-[22px] w-[22px] items-center justify-center rounded border-2`,
tw`${getCheckboxContainerStyle(pressed)} flex size-[22px] items-center justify-center rounded border-2`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lint fix


it('computes baselineOffset correctly for BodyMd', () => {
const variant = MAP_TEXTBUTTON_SIZE_TEXTVARIANT[TextButtonSize.BodyMd];
// eslint-disable-next-line tailwindcss/no-custom-classname
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabling no custom rule for this dynamic usage. We should try to avoid this if possible.

@georgewrmarshall georgewrmarshall force-pushed the cursor/generate-tailwind-config-for-design-system-9a8d branch from 339d0f7 to 1fac47b Compare July 16, 2025 20:03
@github-actions
Copy link
Contributor

📖 Storybook Preview

@@ -1,23 +1,12 @@
const designSystemPreset = require('@metamask/design-system-tailwind-preset');
Copy link
Contributor Author

@georgewrmarshall georgewrmarshall Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactors and streamlines the tailwind-intellisense.config.js used for IntelliSense and the ESLint Tailwind plugin in React Native Storybook stories. Instead of relying on the previous Storybook React Tailwind config, which was similar but not fully compatible, this change imports the canonical Tailwind config directly from @metamask/design-system-twrnc-preset. This ensures the config is accurate, up to date, and fully aligned with the design system used in React Native components, improving both developer experience and consistency across the project.

@@ -26,6 +26,101 @@ or
npm install react@^18.2.0 react-native@0.72.15 twrnc@^4.5.1
```

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating docs to provide instructions on setting up tailwind config for intellisense and eslint tailwind plugin

@github-actions
Copy link
Contributor

📖 Storybook Preview

},
},
// Tailwind ESLint
// Tailwind ESLint for React Web
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separating eslint tailwind plugin settings so we can point to each tailwind config separately

},
},
},
// Tailwind ESLint for React Native
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the same rules for react native but taking in to account twrnc and the new tailwind config

@github-actions
Copy link
Contributor

📖 Storybook Preview

export { useTailwind, useTheme } from './hooks';

// Config generation
export { generateTailwindConfig } from './tailwind.config';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are exporting generateTailwindConfig so we can use it in the tailwind-intellisense.config.js file

@github-actions
Copy link
Contributor

📖 Storybook Preview

@github-actions
Copy link
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall changed the title chore: generate tailwind.config.js for tailwind dev tools based on twrnc preset chore: improve tailwind intellisense and linting for react native Jul 16, 2025
@github-actions
Copy link
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall self-assigned this Jul 16, 2025
Comment on lines +62 to +63
// Keep essential semantic colors, remove default palette colors.
// We want to rely on the colors provided by the design system preset
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small comment update

Comment on lines +66 to +69
black: '#000000',
white: '#ffffff',
black: brandColor.black,
white: brandColor.white,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating to use design token values instead of static hex values

Comment on lines -70 to -73
// Include all design system colors in base theme for IntelliSense
...colors,
},
// This removes all default Tailwind font sizes and weights.
// We want to rely on the design system font sizes and enforce use of the Text component
textColor: {
...colors,
...textColors, // e.g. text-default instead of text-text-default
},
backgroundColor: {
...colors, // Incorporate existing color utilities like bg-primary-default
...backgroundColors, // e.g. bg-default instead of bg-background-default
},
borderColor: {
...colors, // Incorporate existing color utilities like border-primary-default
...borderColors, // e.g. border-default instead of border-border-default
},
fontSize: {
// Empty to remove default Tailwind font sizes (text-sm, text-lg, etc.)
// Design system font sizes added via extend
...typographyTailwindConfig.fontSize,
},
extend: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of extending the default Tailwind theme and configuration, we intentionally override it. This is because the only projects using this package. Specifically, the monorepo storybook and mobile, do not have an existing Tailwind configuration (unlike Portfolio). As a result, we can fully replace Tailwind’s default colors and typography with those provided by the design system, ensuring complete alignment with our design tokens and enforcing consistency across these projects.

Comment on lines +105 to +106
// Export Theme enum for consumers
export { Theme } from './Theme.types';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed for taiwlind-intellisense.config.js setup

@@ -13,7 +13,7 @@ describe('ButtonIcon', () => {
it('renders default state correctly', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lint fixes

Comment on lines +30 to +39
"./tailwind.config": {
"import": {
"types": "./dist/tailwind.config.d.mts",
"default": "./dist/tailwind.config.mjs"
},
"require": {
"types": "./dist/tailwind.config.d.cts",
"default": "./dist/tailwind.config.cjs"
}
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSX vs Node.js: Why We Need the Separate Export

The Problem:

// Main export (packages/design-system-twrnc-preset/src/index.ts)
export { ThemeProvider } from './ThemeProvider';  // ← Contains JSX
export { useTailwind, useTheme } from './hooks';  // ← Contains JSX
export { generateTailwindConfig } from './tailwind.config';  // ← Pure JS

When compiled, the JSX becomes:

// dist/ThemeProvider.cjs
return (<ThemeContext.Provider value={contextValue}>  // ← Node.js can't parse this
  {children}
</ThemeContext.Provider>);

The Issue:

  • React Apps: Can handle JSX syntax ✅
  • Node.js environments: Cannot parse JSX syntax ❌
    • ESLint running tailwind-intellisense.config.js
    • Build tools loading config files
    • Any server-side usage

The Solution:

// package.json exports
{
  ".": "./dist/index.cjs",              // ← Contains JSX (for React apps)
  "./tailwind.config": "./dist/tailwind.config.cjs"  // ← No JSX (for Node.js)
}

Usage Pattern:

// ❌ This crashes in Node.js (ESLint, config files)
const { generateTailwindConfig } = require('@metamask/design-system-twrnc-preset');

// ✅ This works in Node.js
const { generateTailwindConfig } = require('@metamask/design-system-twrnc-preset/tailwind.config');

// ✅ This works in React apps
import { ThemeProvider } from '@metamask/design-system-twrnc-preset';

Why This Matters:

ESLint needs to load tailwind-intellisense.config.js to validate classnames. Since ESLint runs in Node.js, it can't import from the main export that contains JSX components. The separate /tailwind.config export provides a Node.js-safe path to access only the config functions.

TL;DR: Main export has JSX for React apps, separate export has pure JS for Node.js tools like ESLint.

fontFamily: {
...typographyTailwindConfig.fontFamily,
},
extend: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of extending the Tailwind theme and configuration, we actually want to override it. The only projects that will use this package are the monorepo Storybook and Mobile, which don't have existing Tailwind configurations like Portfolio does. So we can fully override the Tailwind colors and typography in favor of the design system–aligned values.

@github-actions
Copy link
Contributor

📖 Storybook Preview

@github-actions
Copy link
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall force-pushed the cursor/generate-tailwind-config-for-design-system-9a8d branch from 22561f1 to 7308553 Compare July 16, 2025 21:23
@github-actions
Copy link
Contributor

📖 Storybook Preview

const expectedTextColor = tw`${TextColor.TextMuted}`.color;
const expectedTextColor = tw`text-muted`.color;
// eslint-disable-next-line tailwindcss/no-custom-classname
const expectedFontSize = tw`text-${TextVariant.BodySm}`.fontSize;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to change this to text-body-sm, but for some reason it passes linting locally and fails in CI. Leaving it as is for now.

That said, we should avoid constructing Tailwind classnames using partial strings or string literals. It's harder to read, harder to debug, and more error-prone. Wherever possible, we should define the full classname explicitly—this improves readability, makes debugging easier, and helps with linting and IntelliSense support.

@georgewrmarshall georgewrmarshall marked this pull request as ready for review July 16, 2025 21:29
@georgewrmarshall georgewrmarshall requested a review from a team as a code owner July 16, 2025 21:29
@georgewrmarshall georgewrmarshall requested a review from Copilot July 16, 2025 21:32
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Enhance React Native Tailwind tooling by introducing a static config generator, packaging updates, docs, and linting support, plus migrating existing components/tests to new shorthand classes.

  • Extend generateTailwindConfig with essential colors and export it
  • Add package exports and usage docs for IntelliSense and ESLint integration
  • Update ESLint config and Storybook setup for React Native, and migrate styles/tests to size-[x] shorthand

Reviewed Changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/design-system-twrnc-preset/src/tailwind.config.ts Enhanced config generator by merging essential and design system colors
packages/design-system-twrnc-preset/src/index.ts Exported generateTailwindConfig from main entry
packages/design-system-twrnc-preset/package.json Added exports for tailwind.config for both ESM and CJS
packages/design-system-twrnc-preset/README.md Added usage guide for ThemeProvider, useTailwind, and config gen
packages/design-system-react-native/src/components/TextButton/TextButton.tsx Added ESLint disable for dynamic Tailwind class in code
packages/design-system-react-native/src/components/TextButton/TextButton.test.tsx Added ESLint disable for dynamic Tailwind class in tests
packages/design-system-react-native/src/components/Checkbox/Checkbox.tsx Switched from h-[22px] w-[22px] to size-[22px] shorthand
packages/design-system-react-native/src/components/Checkbox/Checkbox.test.tsx Updated tests to reflect size-[22px] shorthand
packages/design-system-react-native/src/components/ButtonIcon/ButtonIcon.test.tsx Adjusted expected class order in ButtonIcon test
packages/design-system-react-native/src/components/Box/Box.tsx Simplified tw invocation and removed nested .trim()
packages/design-system-react-native/src/components/Box/Box.test.tsx Updated expected class order in Box tests
packages/design-system-react-native/src/components/BadgeIcon/BadgeIcon.test.tsx Migrated BadgeIcon test to use size-[16px] shorthand
packages/design-system-react-native/src/components/AvatarBase/AvatarBase.test.tsx Simplified imports and updated class usage in AvatarBase test
eslint.config.mjs Added dedicated Tailwind ESLint rules/settings for React Native
apps/storybook-react-native/tailwind-intellisense.config.js Switched from preset to generateTailwindConfig for IntelliSense
Comments suppressed due to low confidence (2)

packages/design-system-twrnc-preset/README.md:34

  • The README references ThemeProvider, but this package's index.ts does not export ThemeProvider. Either update the docs to match the actual exports or add ThemeProvider to the package API.
import {

packages/design-system-twrnc-preset/src/tailwind.config.ts:47

  • There are no direct unit tests for generateTailwindConfig. Adding tests to validate the output structure and key properties for each Theme would help prevent regressions.
export const generateTailwindConfig = (theme: Theme): TwConfig => {

@georgewrmarshall georgewrmarshall added this pull request to the merge queue Jul 16, 2025
@georgewrmarshall georgewrmarshall removed this pull request from the merge queue due to the queue being cleared Jul 16, 2025
@georgewrmarshall georgewrmarshall merged commit 909e747 into main Jul 16, 2025
38 checks passed
@georgewrmarshall georgewrmarshall deleted the cursor/generate-tailwind-config-for-design-system-9a8d branch July 16, 2025 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants