Skip to content

chore: implement tailwind eslint#760

Merged
georgewrmarshall merged 2 commits into
mainfrom
fix/eslint-tailwind
Jul 8, 2025
Merged

chore: implement tailwind eslint#760
georgewrmarshall merged 2 commits into
mainfrom
fix/eslint-tailwind

Conversation

@georgewrmarshall

@georgewrmarshall georgewrmarshall commented Jun 24, 2025

Copy link
Copy Markdown
Contributor

Description

This PR improves the linting and code quality of our Tailwind CSS usage by replacing Prettier-based formatting with comprehensive ESLint rules. The changes enforce Tailwind CSS best practices, catch common errors, and ensure consistent utility class usage across the codebase.

Key improvements:

  1. Replaced Prettier plugin with ESLint plugin - Moved from prettier-plugin-tailwindcss to eslint-plugin-tailwindcss for better error detection and enforcement
  2. Added comprehensive Tailwind ESLint rules including:
    • classnames-order: Enforces consistent class ordering
    • enforces-shorthand: Requires shorthand utilities (e.g., size-4 instead of h-4 w-4)
    • no-custom-classname: Catches invalid/custom class names
    • no-contradicting-classname: Prevents conflicting utility classes
    • no-arbitrary-value: Originally tried to enforce this to prevent static colors e.g. bg-[#000] but we legitimately need it for sizes, animations, etc. We can look at using our own eslint-plugin-design-tokens for this like portfolio does.
  3. Fixed existing code quality issues throughout the codebase:
    • Replaced h-full w-full with size-full shorthand
    • Consolidated px-4 py-4 to p-4 shorthand
    • Fixed invalid custom class names in tests to use valid Tailwind utilities
    • Corrected border utility syntax
  4. Simplified configuration - Removed complex Prettier overrides and consolidated Tailwind linting into a single ESLint configuration

Why this change:

  • ESLint provides better error detection and prevents invalid Tailwind usage at development time
  • Enforces consistent shorthand usage for better maintainability
  • Catches typos and invalid class names that could cause styling issues
  • Reduces bundle size by preventing unused/invalid classes
  • Provides better developer experience with clear error messages

Related issues

Fixes: #738

Manual testing steps

  1. Run yarn lint to verify ESLint rules are working correctly
  2. Try adding invalid Tailwind classes (e.g., bg-invalid-color) and confirm ESLint catches them
  3. Try using longhand utilities (e.g., h-4 w-4) and verify ESLint suggests shorthand (size-4)
  4. Run Storybook for both React and React Native to ensure all components still render correctly
  5. Verify that existing tests pass with the updated class names
  6. Check that the linter catches contradicting classes (e.g., flex hidden)

Screenshots/Recordings

Before

  • Prettier plugin only handled formatting, not error detection
  • Complex configuration with multiple overrides for different file patterns
  • No enforcement of Tailwind best practices
  • Custom/invalid class names could slip through

After

  • ESLint actively catches Tailwind errors during development
  • Simplified, single configuration for all Tailwind files
  • Enforced shorthand usage and consistent patterns
  • Invalid class names are caught before they reach production

React

after.react.eslint720.mov

React native

after.react.native.storybook.lint720.mov

twMerge

twmerge.eslint.mov

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs
  • I've completed the PR template to the best of my ability
  • I've included tests if applicable
  • I've documented my code using JSDoc format if applicable
  • I've applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

@socket-security

socket-security Bot commented Jun 24, 2025

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​eslint-plugin-tailwindcss@​3.17.0981006276100
Addedeslint-plugin-tailwindcss@​3.18.09910010078100
Updatednanoid@​3.3.8 ⏵ 3.3.1110010080 -1983100
Addedpostcss@​8.5.6991008191100

View full report

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Comment thread .prettierrc.js
Comment on lines -11 to -42
// Tailwind CSS
plugins: ['prettier-plugin-tailwindcss'],
tailwindFunctions: ['tw', 'twMerge'], // Support twrnc and twMerge template literals and utility functions
tailwindAttributes: ['twClassName'], // Support twClassName prop

// Use Prettier's configuration overrides to handle multiple Tailwind configs
// This allows different files to use different Tailwind configurations
// See: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/335#issuecomment-2801686781
overrides: [
{
// React web components and stories use the Storybook React Tailwind config
files: [
'./packages/design-system-react/src/**',
'./apps/storybook-react/stories/**',
'./packages/design-tokens/stories/**',
],
options: {
tailwindConfig: './apps/storybook-react/tailwind.config.js',
},
},
{
// React Native components and stories use the TWRNC preset Tailwind config
files: [
'./packages/design-system-react-native/src/**',
'./apps/storybook-react-native/stories/**',
],
options: {
tailwindConfig:
'./apps/storybook-react-native/tailwind-intellisense.config.js',
},
},
],

@georgewrmarshall georgewrmarshall Jun 29, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removing all Tailwind Prettier-related configurations from the Prettier config, as we're no longer using the Prettier Tailwind plugin. The ESLint plugin now handles class ordering and enforces additional formatting.

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

TWCLASSMAP_AVATARBASE_SIZE_DIMENSION[AvatarBaseSize.Md],
].join(' ');
const customClasses = `${baseClasses} extra-class`;
const customClasses = `${baseClasses} bg-default`;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because we are enforcing no non-tailwind classnames we have to update tests with mock clasnames to valid tailwind classnames

Comment on lines +2 to +5
import { promises as fs } from 'fs';
import * as path from 'path';
/* eslint-enable import-x/no-nodejs-modules */
import postcss from 'postcss';
import { parse } from 'postcss';

@georgewrmarshall georgewrmarshall Jun 29, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Eslint violations fix. Fixes import-x/no-named-as-default-member: 0 -> 1 (+1) eslint violation

Screenshot 2025-06-29 at 2 42 34 PM

}) => (
<div
className={`grid h-[100px] place-content-center rounded bg-default text-center ${className}`}
className={`grid h-24 place-content-center rounded bg-default text-center ${className}`}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Even though we are allowing arbitrary values we should try to use the spacing and size system provided by tailwind as much as possible to reduce the amount of CSS that is generated

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

// 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
fontSize: {},
fontWeight: {},

@georgewrmarshall georgewrmarshall Jun 30, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We want to enforce the usage of only the preset classnames for font weights e.g. font-regular, font-medium, font-bold. The twMerge utility was still allowing for all default tailwind font weight names and allowing for variant font weights which we've removed.

white: '#ffffff',
},
fontSize: {}, // This removes all default Tailwind font sizes. We want to rely on the design system font sizes and enforce use of the Text component
extend: {},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removing extend as I don't think it's needed

Comment on lines +13 to +14
// Keep essential semantic colors, remove default palette colors.
// We want to rely on the colors provided by @metamask-previews/design-system-tailwind-preset

@georgewrmarshall georgewrmarshall Jun 30, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adding a line break here to make comments easier to read

<Box twClassName="w-full bg-default py-4">
{/* Header */}
<Box twClassName="border-b border-muted px-4 py-4">
<Box twClassName="border-b border-muted p-4">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

enforces-shorthand

decorators: [
(Story) => (
<Box className="m-[-1rem]">
<Box className="-m-4">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no-unnecessary-arbitrary-value

<Box className="mx-auto w-full bg-default md:max-w-xl md:rounded-3xl md:py-4">
{/* Header */}
<Box className="border-color-border-muted px-4 py-4 md:px-8">
<Box className="border-b border-muted p-4 md:px-8">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no-custom-classname this rule is one of the most important IMO it catches invalid tailwind classnames. border-color-border-muted was invalid but it was not being picked up.

Comment thread eslint.config.mjs
Comment on lines +284 to +291
settings: {
tailwindcss: {
callees: ['tw', 'twMerge', 'twClassName'],
config: 'apps/storybook-react/tailwind.config.js',
classRegex: '^class(Name)?$',
},
},
},

@georgewrmarshall georgewrmarshall Jun 30, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We should have good coverage for both React Native and React libraries, but a few points to note:

  • We're still using the React Tailwind config for linting, so some classes not supported by twrnc may not be flagged. This will be addressed in Improve Tailwind IntelliSense for React Native Development #737)
  • Classnames in our *constant.ts files aren't being caught by the plugin. I've tried several regex approaches without success and have opened an issue feat: Add Tailwind class validation for constant files #763
  • The tw`` template literal syntax in twrnc won't work with the ESLint Tailwind plugin because the plugin is primarily designed to validate class names in static string contexts (like classNameortwClassNameprops). When using template literals with thetwtag, the class names are processed at runtime and potentially transformed into React Native style objects, making it harder for ESLint's static analysis to reliably extract and validate the class names. This is why the plugin'sclassRegex` pattern works well with props but struggles with template literal tags, as it can't properly parse the dynamic nature of tagged template literals in its AST traversal. twrnc will take care of the invalid classnames but we won't get the other eslint rule benefits

@@ -1,3 +1,4 @@
/* eslint-disable tailwindcss/no-contradicting-classname */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We are actively testing how twMerge utility handles contradictory classnames so we want to disable the eslint rule for this test file

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Comment on lines 39 to +47
describe('font weight conflicts', () => {
it('should handle standard Tailwind font weight overrides', () => {
const result = twMerge('font-bold font-normal');
expect(result).toBe('font-normal');
});

it('should handle custom typography font weight overrides', () => {
const result = twMerge('font-s-body-md font-l-heading-lg');
expect(result).toBe('font-l-heading-lg');
const result = twMerge('font-bold font-medium');
expect(result).toBe('font-medium');
});

it('should handle mixed standard and custom font weight overrides', () => {
const result = twMerge('font-s-body-md font-bold');
expect(result).toBe('font-bold');
const result = twMerge('font-bold font-regular');
expect(result).toBe('font-regular');

@georgewrmarshall georgewrmarshall Jun 30, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Simplifying tests because we now only support 3 font weight classnames that are provided by the tailwind preset package: font-regular(custom), font-medium and font-bold.

  describe('font weight conflicts', () => {
    it('should handle standard Tailwind font weight overrides', () => {
      const result = twMerge('font-bold font-medium');
      expect(result).toBe('font-medium');
    });

    it('should handle mixed standard and custom font weight overrides', () => {
      const result = twMerge('font-bold font-regular');
      expect(result).toBe('font-regular');
    });
  });

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

'font-extralight',
'font-light',
'font-normal',
'font-regular',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

font-regular is the only custom font weight classname that we support

Comment on lines -55 to -58
// Custom typography classes
{
font: variantClassGroups,
},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We have removed variant based font weight classnames. e.g. font-s-display-lg so these are no longer needed

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall marked this pull request as ready for review June 30, 2025 17:54
@georgewrmarshall georgewrmarshall requested a review from a team as a code owner June 30, 2025 17:54
@github-actions

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall changed the title chore: implement tailwind eslint in favor of prettier chore: implement tailwind eslint Jul 4, 2025
@github-actions

github-actions Bot commented Jul 4, 2025

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

cursor[bot]

This comment was marked as outdated.

Comment thread .depcheckrc.yml
- 'eslint-plugin-*'
- 'jest-silent-reporter'
- 'prettier-plugin-packagejson'
- 'prettier-plugin-tailwindcss'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed all instances of prettier-plugin-tailwindcss

Comment on lines +282 to +284
- **ESLint Integration**: Use `eslint-plugin-tailwindcss` for Tailwind class validation and ordering
- **Consistent Ordering**: Maintain consistent class ordering through ESLint rules
- **Class Validation**: ESLint enforces proper class usage, prevents contradictory classes, and encourages shorthand utilities

Copy link
Copy Markdown
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 reference eslint tailwind plugin instead of prettier-plugin-tailwindcss

@github-actions

github-actions Bot commented Jul 4, 2025

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall merged commit 2439b58 into main Jul 8, 2025
38 checks passed
@georgewrmarshall georgewrmarshall deleted the fix/eslint-tailwind branch July 8, 2025 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement ESLint Tailwind CSS Plugin for Enhanced Class Validation

2 participants