Created
February 11, 2026 14:26
-
-
Save JWPapi/9cf47f55671eaea3107b6d20ecd6e940 to your computer and use it in GitHub Desktop.
ESLint rule: no-hover-translate - Disallows hover translate effects in Tailwind CSS (causes chase UX bug)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * ESLint rule: no-hover-translate | |
| * | |
| * Disallows hover translate effects in Tailwind CSS classes. | |
| * These create a "chase" effect where elements move away from the cursor, | |
| * especially when approaching from below. | |
| * | |
| * Bad: hover:-translate-y-1, hover:translate-y-2, group-hover:-translate-y-0.5 | |
| * Good: hover:shadow-md, hover:scale-105 (if you need hover feedback) | |
| */ | |
| export default { | |
| rules: { | |
| 'no-hover-translate': { | |
| meta: { | |
| type: 'problem', | |
| docs: { | |
| description: 'Disallow hover translate effects which cause chase/escape UX issues' | |
| }, | |
| messages: { | |
| noHoverTranslate: | |
| "Don't use '{{ className }}' — hover translate creates a chase effect where elements move away from the cursor. Use hover:shadow-* instead for hover feedback." | |
| }, | |
| schema: [] | |
| }, | |
| create(context) { | |
| const hoverTranslatePattern = /\b(group-)?hover:-?translate-[xy]-/g | |
| function checkString(value, node) { | |
| const matches = value.match(hoverTranslatePattern) | |
| if (matches) { | |
| for (const match of matches) { | |
| context.report({ | |
| node, | |
| messageId: 'noHoverTranslate', | |
| data: { className: match.replace(/-$/, '-*') } | |
| }) | |
| } | |
| } | |
| } | |
| function checkNode(node) { | |
| if (!node) return | |
| if (node.type === 'Literal' && typeof node.value === 'string') { | |
| checkString(node.value, node) | |
| } else if (node.type === 'TemplateLiteral') { | |
| for (const quasi of node.quasis) { | |
| checkString(quasi.value.raw, node) | |
| } | |
| } else if (node.type === 'ConditionalExpression') { | |
| checkNode(node.consequent) | |
| checkNode(node.alternate) | |
| } else if (node.type === 'BinaryExpression' && node.operator === '+') { | |
| checkNode(node.left) | |
| checkNode(node.right) | |
| } | |
| } | |
| return { | |
| JSXAttribute(node) { | |
| if ( | |
| node.name && | |
| node.name.name === 'className' && | |
| node.value | |
| ) { | |
| if (node.value.type === 'Literal') { | |
| checkString(node.value.value, node.value) | |
| } else if (node.value.type === 'JSXExpressionContainer') { | |
| checkNode(node.value.expression) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment