Design Token Lint
GitHub repository

Type to search...

to open search from anywhere

Known Limitations

Inherent limitations of the static analysis approach used by design-token-lint.

design-token-lint uses bounded static source scanning and regex-based matching to extract class names from source files. This approach is fast and requires no build step, but it cannot evaluate code at runtime. As a result, some dynamic patterns are not correctly analyzed.

The set of attribute names and utility functions scanned is configurable via classAttributes and classFunctions in your config file. By default, className, class, cn, clsx, classNames, and twMerge are scanned.

These are not bugs — they are inherent to static analysis.

Supported: Multiline className

Multiline className values are supported:

<div
  className="
    p-4
    bg-gray-500
  "
>

Classes spread across multiple lines are extracted correctly.

Supported: Object Keys in class:list

Astro's class:list object syntax is supported — class names in quoted keys are extracted:

<div class:list={[{ "p-4": true, "m-8": isActive }]}>

Both p-4 and m-8 are extracted and linted.

Supported: Multiline Function Calls

cn(), clsx(), classNames(), and twMerge() calls that span multiple lines are supported — arguments are accumulated until the parentheses balance, the same way multiline className values are:

<div
  className={cn(
    "p-4",
    "bg-gray-500",
    isActive && "m-8",
  )}
>

All three string arguments are extracted and linted. This also applies to class:list={[...]} arrays that span multiple lines.

Limitation: Vue/Svelte Dynamic Class Bindings

Static class="..." attributes are extracted correctly in any template language, including Vue and Svelte. Dynamic binding syntax is not:

<!-- Not linted correctly — extracted as garbage tokens, no real violations reported -->
<div :class="isActive ? 'p-4' : 'm-8'">
<div v-bind:class="{ 'p-4': isActive }">
<!-- Not linted at all — the `class:` directive isn't recognized, nothing is extracted -->
<div class:active={isActive} class:p-4={isBig}>

Vue's :class/v-bind:class bindings match the class attribute pattern (the leading : doesn't block the match), so their expression content is extracted and split into tokens — but the result is punctuation-laden garbage (isActive, ?, 'p-4', :, 'm-8') that never matches a prohibited pattern, so violations inside it are silently missed. Svelte's class:name directive isn't matched at all (class isn't followed by =), so it's skipped entirely — also with no violations reported.

Workaround: Extract the relevant classes into a static string (e.g. via a computed property or a cn()/clsx() call assigned to a variable) so the linter sees a plain string literal, or add an ignore comment to acknowledge the gap.

Multiline Attribute Expressions

Configured class attributes with balanced {...} expressions are scanned across physical lines. The scanner follows nested arrays, .join(...) calls, conditionals, nested delimiters, and configured class-function calls, while preserving the physical line of each literal:

export const B = () => (
  <div
    className={[
      'flex items-center',
      'z-40',
    ].join(' ')}
  />
);

export const C = ({ active }: { active: boolean }) => (
  <div
    className={`
      flex items-center
      ${active ? 'z-40' : ''}
    `}
  />
);

Both z-40 literals are linted at their source line. Literal strings in conditional branches are checked even when the expression spans multiple lines. The scanner remains bounded to 50 physical lines for malformed/unclosed expressions; input beyond that cap is left unscanned rather than parsed as unrelated source.

Dynamic Template Segments

Runtime-generated class names cannot be evaluated statically:

// The runtime value is unknown, so no complete class name can be checked.
<div className={`p-${size} bg-${color}-500`}>

Literal class text written inside an interpolation expression is still checked, so a conditional such as ${active ? 'z-40' : ''} is covered. Refactor truly dynamic names to a finite set of literal branches when they need lint coverage, or add an ignore comment to acknowledge the dynamic boundary.

Limitation: Escaped Quotes

Class attributes containing escaped quotes may extract incorrectly:

// May extract incorrectly
<div className="p-4 \"m-8\"">

The regex parser does not handle escaped quote sequences inside string literals.

Workaround: Avoid escaped quotes inside class attributes. Use JSX expression syntax instead:

<div className={'p-4 m-8'}>

Limitation: CSS/SCSS Scanning (v1, Declaration-Based Only)

The opt-in css config section scans plain CSS/SCSS declaration values (zIndex, colorLiterals) using the same line/regex-based approach as the Tailwind extractor — no PostCSS, no AST. It correctly handles /* */ comments (including multi-line spans), string literals, url(...) values, and SCSS // line comments. The following are known false negatives in v1 — a literal is present but not flagged, by design:

/* Not flagged — custom-property and SCSS-variable definitions are treated as
   token-definition sites, not usage sites */
:root {
  --brand: #f00;
}
.a {
  $brand: #f00;
}

/* Not flagged — SCSS maps and other nested/interpolated SCSS constructs */
.a {
  $colors: (primary: #f00, secondary: #0f0);
}

Distinguishing a palette-definition zone (:root, @theme, a SCSS variable declaration) from a semantic-token usage site that should reference the palette instead needs zone-aware scanning — a deferred later pass, not part of v1. Values split across exotic multi-line syntax beyond a single declaration are also not guaranteed to be caught.

This is declaration-value scanning only — it says nothing about Tailwind utility classes inside <style> blocks or CSS-in-JS template literals; those remain outside the css section's scope entirely (the Tailwind class extractor doesn't scan .css/.scss files, and the CSS declaration rules don't scan .tsx/.jsx files).

Summary

PatternSupported
Multiline classNameYes
Multiline cn()/clsx()/class:list callsYes
Static string literalsYes
Multiline {...} class attributes (arrays, conditionals, nested calls)Yes — literal segments
Object keys in class:listYes
Custom attribute names (via classAttributes)Yes — configurable
Custom utility functions (via classFunctions)Yes — configurable
Vue/Svelte static class="..." attributesYes
CSS/SCSS declaration scanning (opt-in, css section)Yes — declaration-based only, see above
Runtime-generated template segmentsNo — complete dynamic names cannot be evaluated
Escaped quotes in class stringsNo — may extract incorrectly
Vue :class/v-bind:class bindingsNo — extracted as garbage tokens
Svelte class:name directivesNo — not recognized at all
CSS custom-property/SCSS-variable definitions holding a literalNo — token-definition zone awareness deferred
SCSS maps and nested/interpolated constructsNo — out of scope for v1

Revision History

CreatedUpdated