npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

motif-lint

v0.1.0

Published

CLI linter for UI/UX design system drift — like ESLint, but for your design tokens

Readme

Motif

ESLint for your design system. Catch UI/UX drift before it ships.

Motif statically analyzes your JavaScript and TypeScript source files and flags violations like hardcoded colors, raw spacing values, inline styles, and class names that don't belong to your design system — all without depending on ESLint.

src/components/Button.tsx
  line 12  error   Hardcoded color "#ff0000" for "color" — use a design token instead   no-hardcoded-colors
  line 18  warn    Class "btn-special" is not in the allowed list                        no-custom-classes

src/pages/Home.jsx
  line 34  warn    Inline style detected — use a design token or utility class instead   no-inline-styles

3 problems (1 error, 2 warnings)

Install

npm install --save-dev motif-lint
# or
yarn add -D motif-lint
# or
pnpm add -D motif-lint

Requirements: Node.js >= 18.18.0


Quick Start

# Scaffold a config file
npx motif init

# Lint your entire project
npx motif lint

# Lint a specific directory
npx motif lint ./src

# Watch mode — re-lint on every save
npx motif lint --watch

Add to package.json:

{
  "scripts": {
    "lint:ui": "motif lint ./src",
    "lint:ui:ci": "motif lint ./src --format json --max-warnings 0"
  }
}

Configuration

Motif looks for motif.config.js or .motifrc.json starting in the target directory and walking up to the filesystem root.

motif.config.js

module.exports = {
  // Rule severities: 'error', 'warn', or 'off'
  // Array form passes options to the rule: ['error', { ...options }]
  rules: {
    'no-hardcoded-colors': 'error',
    'no-custom-classes': ['warn', { allowedPrefixes: ['tw-'] }],
    'use-token-values': ['error', { allowedValues: ['0', '100%'] }],
    'no-inline-styles': 'warn',
  },

  tokens: {
    // Your CSS custom property names
    colors: ['--color-primary', '--color-danger', '--color-neutral-100'],
    spacing: ['--spacing-xs', '--spacing-sm', '--spacing-md', '--spacing-lg'],

    // Class names your design system exports
    allowedClasses: ['btn', 'btn-primary', 'card', 'text-heading'],
  },

  // Glob patterns to ignore
  ignore: ['**/node_modules/**', '**/dist/**', '**/*.test.tsx'],

  // Load all rules from a local directory
  rulesDir: './motif-rules',

  // Inline custom rules
  customRules: {
    'no-legacy-button': require('./motif-rules/no-legacy-button'),
  },

  // npm plugin packages
  plugins: ['motif-plugin-tailwind'],
}

Severity values

| Value | Meaning | |-------|---------| | 'error' | Violation — exits with code 1 | | 'warn' | Warning — exits with code 0 (unless --max-warnings exceeded) | | 'off' | Rule disabled |


Built-in Rules

| Rule | Type | Default | Description | |------|------|---------|-------------| | no-hardcoded-colors | problem | error | Disallow hardcoded hex/rgb/hsl color values | | use-token-values | problem | error | Disallow raw dimension values (px, rem…) — use tokens | | no-custom-classes | suggestion | warn | Enforce use of design system class names only | | no-inline-styles | suggestion | warn | Disallow inline style props |


CLI Reference

motif lint [path]

Lint files for design system violations.

| Flag | Description | |------|-------------| | --watch | Re-lint on file changes | | --config <path> | Path to a config file | | --format <fmt> | Output format: stylish (default), json, compact | | --quiet | Report only errors, suppress warnings | | --max-warnings <n> | Exit with code 1 if warnings exceed n |

Exit codes:

  • 0 — No errors (warnings don't affect exit code unless --max-warnings is set)
  • 1 — One or more errors found

Examples:

# Lint and fail on any warning (useful in CI)
motif lint --max-warnings 0

# JSON output for CI tooling
motif lint --format json > motif-report.json

# Errors only (no warnings in output)
motif lint --quiet

# Use a non-default config
motif lint --config .motifrc.staging.json

motif rules [path]

List all available rules with their current severity.

motif rules
motif rules ./packages/web

motif init

Scaffold a motif.config.js in the current directory.


Writing Custom Rules

Custom rules follow the same API as built-in rules. Drop a .js file anywhere and reference it in your config.

Rule structure

// motif-rules/no-legacy-button.js
module.exports = {
  meta: {
    name: 'no-legacy-button',
    type: 'problem',           // 'problem' | 'suggestion'
    docs: {
      description: 'Disallow the deprecated <Button> component — use <DSButton> instead',
      category: 'Components',
    },
    fixable: null,             // 'code' when auto-fix is supported (future)
    schema: [],                // JSON Schema array for options validation
  },

  create(context) {
    // context.options  — rule options from config (array form: ['error', { ...opts }])
    // context.settings — full config object (tokens, etc.)
    // context.filename — path of the file being linted
    // context.report() — file a violation

    return {
      // Visitor methods match AST node types.
      // Any @typescript-eslint/parser node type works.
      JSXOpeningElement(node) {
        const name = node.name?.name
        if (name === 'Button') {
          context.report({
            node,
            message: '<Button> is deprecated — use <DSButton> from @acme/ds instead',
          })
        }
      },
    }
  },
}

Loading custom rules

Option 1 — inline in config:

// motif.config.js
module.exports = {
  rules: {
    'no-legacy-button': 'error',
  },
  customRules: {
    'no-legacy-button': require('./motif-rules/no-legacy-button'),
  },
}

Option 2 — rules directory (all .js files loaded automatically):

module.exports = {
  rules: {
    'no-legacy-button': 'error',
    'no-old-icon': 'warn',
  },
  rulesDir: './motif-rules',   // loads no-legacy-button.js, no-old-icon.js, etc.
}

Rule options

Rules can accept per-project options using the array config form:

// In motif.config.js:
rules: {
  'my-rule': ['error', { someOption: true, values: ['a', 'b'] }],
}
// In the rule:
create(context) {
  const { someOption, values } = context.options
  // ...
}

Available AST node types

Motif uses @typescript-eslint/parser — any node type it produces works as a visitor key. Useful ones:

| Node type | When it fires | |-----------|--------------| | JSXAttribute | Any JSX prop: style=, className=, onClick=, etc. | | JSXOpeningElement | Opening tag: <Button>, <div>, etc. | | JSXElement | Full JSX element | | Literal | String/number literals in JS | | Identifier | Variable names | | ImportDeclaration | import statements | | CallExpression | Function calls | | TemplateLiteral | Template strings |

Browse the full list: TypeScript ESLint AST spec


Plugin Authoring

Share rules across teams or publish them on npm.

Plugin structure

// motif-plugin-acme/index.js
module.exports = {
  name: 'acme',           // prefix for all rules in this plugin
  rules: {
    'no-legacy-button': require('./rules/no-legacy-button'),
    'use-ds-icons': require('./rules/use-ds-icons'),
  },
}

Using a plugin

// motif.config.js
module.exports = {
  plugins: [
    'motif-plugin-acme',             // npm package name
    // or pass the object directly:
    require('./local-plugin'),
  ],
  rules: {
    'acme/no-legacy-button': 'error',
    'acme/use-ds-icons': 'warn',
  },
}

Publishing

Name your package motif-plugin-<name> by convention. Your package.json:

{
  "name": "motif-plugin-acme",
  "main": "index.js",
  "peerDependencies": {
    "motif": ">=0.1.0"
  }
}

CI Integration

GitHub Actions

- name: Lint design system
  run: npx motif lint --format json --max-warnings 0 > motif-report.json
  continue-on-error: true

- name: Upload report
  uses: actions/upload-artifact@v4
  with:
    name: motif-report
    path: motif-report.json

Pre-commit hook (with Husky)

npx husky add .husky/pre-commit "npx motif lint --quiet"

Supported File Types

| Extension | Parser | |-----------|--------| | .js | @typescript-eslint/parser | | .ts | @typescript-eslint/parser | | .jsx | @typescript-eslint/parser + JSX | | .tsx | @typescript-eslint/parser + JSX |


Contributing

Pull requests welcome. For major changes, open an issue first.

git clone https://github.com/rjcuff/motif
cd motif
npm install
node bin/motif.js lint ./src

License

MIT