Skip to content
Brok UI

Loading…

No results

Lint

php artisan ui:check lints your Blade views against the design system. Your components stay flexible; you decide how they may be used. When a class breaks the rules, the finding says what to use instead: the component's variants and sizes, the nearest theme token, the scale step with the same value, or the spelling you meant. Agents read the same text, so a violation costs one correction round, not a guess.

Note

The linter runs without a Tailwind build. It reads your installed resources/css/ui.css (and its imports) for tokens, @utility names, custom variants and class selectors, and your ui-lock.json for each component's contract.

Run it

terminal
php artisan ui:check
php artisan ui:check --fail-on=warning          # CI gate
php artisan ui:check --format=sarif > brok.sarif # editors and code scanning
php artisan ui:check --json                      # agents and scripts
php artisan ui:check --explain no-restyle        # one rule, its options, its docs
php artisan ui:check --list-rules                # every rule with its effective level
php artisan ui:check --fix                       # apply exact replacements and spelling fixes, then re-check
php artisan ui:check --fix --unsafe-fixes        # also apply approximations (nearest theme colour)

Every finding carries a stable id (UI-408), a name (no-restyle), the file and line, the guidance, and a link to this documentation. A finding that knows the exact replacement carries a fix; --fix applies the safe ones (a scale step with the same value, a spelling correction, the logical twin of a physical utility) and re-checks. A nearest-colour suggestion is an approximation and needs --unsafe-fixes.

terminal
UI-408 resources/views/orders/edit.blade.php:12 ........ ERROR no-restyle
    "p-4" is not allowed on <button>: <button> owns its spacing. Use a size (sm, md, lg, icon),
    or margin on it, gap on the parent element, or padding on the container for space around it.

Configure

Add a lint block to ui.json. Each rule takes a level (off, warning, error) or [level, options]. Rules are keyed by name or id; a rule you do not list keeps its default. Component internals under resources/views/components/ui are outside the default scan, so components can style themselves; a rule's include adds paths it should also run on (keep no-raw-colors and no-unknown-classes on inside the component directory, as shadcn recommends), and exclude takes paths away from any rule. Both accept project-relative prefixes or globs.

ui.json JSON
{
  "lint": {
    "note": "See docs/design-rules.md for approved exceptions.",
    "rules": {
      "no-restyle": ["error", {
        "allow": ["layout"],
        "contracts": [
          { "pattern": "^card\\.title$", "allow": ["layout", "typography"], "deny": ["font-*"] },
          { "pattern": "^card\\.content$", "allow": ["layout", "spacing"] }
        ]
      }],
      "no-raw-colors": "error",
      "no-arbitrary-values": ["warning", { "allow": ["layout"] }],
      "no-inline-styles": ["error", { "allow": ["transform"] }],
      "no-unknown-classes": ["warning", { "include": ["resources/views/components/ui"] }],
      "require-static-classes": "error",
      "no-dark-color-override": "off",
      "no-physical-direction": ["warning", { "exclude": ["resources/views/legacy/**"] }]
    }
  }
}

A mistake in the block, such as a misspelled rule, an unknown option or a category typo, is reported as UI-006 invalid-config on ui.json, and the affected rule pauses until it is fixed. Nothing is silently ignored.

The policy

The class rules share one option shape. allow exempts matching classes, deny removes matches from allow, and contracts set a policy for the components a pattern matches.

ConfigurationPolicy
neitherNo exceptions.
allow: [...]Only matching classes are exempt.
deny: [...]Everything except these matches is exempt.
allow + denyAllow minus deny.

Entries are categories (layout, color, typography, spacing, shape, effects, motion), class groups (px, bg-color, rounded), or classes and wildcard patterns (p-4, p-*, md:p-*). An entry without a colon matches the base class, ignoring variants, important markers, negative prefixes and opacity modifiers; an entry with a colon matches the whole class. Margin, sizing, position, transforms and text alignment are layout; padding and gap are spacing.

A contract's pattern is a regex on the component name as written after the prefix: ^button$ matches only <brok:button>; ^card\. matches every card part. A contract replaces the keys it writes and inherits the rest; when several match, the last one applies. For no-inline-styles the entries are CSS property names (transform, border-*, --*).

Your own words

Every rule accepts message. no-restyle also accepts an object keyed by category with a default. A contract can carry its own message. The linter picks the contract's category message, then its default, then the rule's message, then the built-in guidance. {{key|fallback}} fills an empty slot; an unknown key stays literal and a near-miss is warned about. note is appended to every finding.

ui.json JSON
"no-restyle": ["error", {
  "allow": ["layout"],
  "message": {
    "spacing": "Use a {{component}} size: {{sizes|none defined}}.",
    "default": "Use a {{component}} variant: {{variants|none defined}}."
  }
}],
"no-raw-colors": ["error", {
  "message": "Use a theme color for \"{{className}}\". See {{file}}."
}]
PlaceholderValue
classNameThe class, or an attribute such as fill="#f00".
componentThe component name, empty on plain elements.
propertyThe inline CSS property.
suggestionsNearby tokens, the scale replacement, or a spelling correction.
fileThe theme CSS, or the recipe file for variants.
category, variants, sizes, entries, aroundOn no-restyle findings.
tokens, suggestion, replacement, attribute, valueOn token and class findings that provide them.

Suppress one line

A reasoned comment on the preceding line suppresses one rule by name or id. Malformed, unknown and unused suppressions fail as UI-005, so an exception never outlives its reason.

orders/edit.blade.php Blade
{{-- ui-lint-disable-next-line no-raw-colors: Brand moment approved by design, see DES-142. --}}
<span class="bg-pink-500">New</span>

For agents

Findings are written to be acted on, never to be argued with: no message asks the reader to change the policy. --json returns every finding with its meta (the component, the class, the allowed values, the suggestions) and a docs link. --explain <rule> prints the rule's contract, options and effective level. --path takes a single file, --stdin=<path> lints an unsaved buffer, and --watch re-runs on every change, so the loop stays under a second. Put php artisan ui:check --json in your agent instructions and let it run after each edit; the MCP server exposes the same rule catalog.

Does it pay off?

We ran the shadcn/lint eval method against ui:check: three Blade tasks (a settings page, a pricing table, an orders table), an agent with no tools that answers with the file, up to three correction rounds, and the shipped linter as the only judge. The feedback after each round was either a generic "it still breaks the design rules" or the exact ui:check findings.

Model, feedbackReached zeroRounds (mean)Cost per task at API list price (mean)
Sonnet 5, generic nudge3 / 121.3 when it did$0.36 – $0.55
Sonnet 5, ui:check findings12 / 120.5 – 1.3$0.08
Opus 5, generic nudge1 / 63.0 when it did$0.56 – $0.75
Opus 5, ui:check findings5 / 60.5 – 1.0$0.25 – $0.26

With the findings in the loop the agent reached zero almost always in one round; with a nudge it fixed what it guessed and left the rest, round after round. The findings loop cost 55 to 86 % less per task (token cost at API list price, as the CLI reports it). Small sample (36 cells, September 2026); the harness, the raw cells and the caveats are in packages/evals/lint.

Editors

The Brok Lint extension for VS Code (packages/vscode-brok-lint) runs ui:check --stdin on the unsaved buffer as you type and shows each finding on its class, with the rule id as the code, the docs page one click away, and a quick fix wherever the finding carries one; "Apply safe lint fixes in file" applies them all. Any editor that reads SARIF can use ui:check --format=sarif instead.

JSX and the extension kit

@brokui/eslint-plugin holds Preact and React code to the same six class rules: the engines are shadcn/lint's, the defaults, the UI-nnn ids and the docs links are this catalog's, so a finding on a <Button> in the extension kit reads like one on <brok:button>. Add recommended() from the package to your flat ESLint config.

Rules

Every rule has a page with examples, options and limits. Six rules police classes with the shared policy; the rest read component contracts, accessibility and Livewire usage.

Tokens and classes

UI-301 no-raw-colors Use theme colours. Reports raw palette colours, undeclared colour tokens, and literal colours in SVG attributes. warning
UI-302 no-raw-spacing Spacing must come from the approved scale, not inline lengths or off-scale steps. warning
UI-303 no-raw-z-index Use the semantic z-index layers, never numeric z-index values. warning
UI-304 no-physical-direction Use logical utilities (ms-, pe-, start-) so layouts mirror under RTL. warning
UI-305 no-dark-color-override Dark mode is driven by tokens, not per-utility dark: colour overrides. warning
UI-306 no-arbitrary-values Use theme tokens and scale values instead of arbitrary values such as p-[13px]. warning
UI-307 no-inline-styles Style through classes. Reports inline style properties, unreadable style bindings, and <style> elements. warning
UI-308 no-unknown-classes Catch class names and variants the project's Tailwind cannot generate, with spelling suggestions. warning
UI-309 require-static-classes Class values on design-system components must be readable by the linter. warning

Component contracts

UI-401 no-invalid-variant A variant or size prop must use one of the values the component defines. error
UI-402 no-deprecated-component The component is deprecated; use its replacement. warning
UI-403 no-deprecated-api The prop or slot is deprecated; follow the published migration. warning
UI-404 no-internal-classes Consumer views must not reference internal package classes. error
UI-405 no-local-fork An application component looks like a copy of an installed one; extend it instead. warning
UI-406 no-unknown-component The component is not installed or is missing from ui-lock.json. error
UI-407 no-invalid-component-api A prop, slot, or child must match the component's published API. error
UI-408 no-restyle Classes on a design-system component are limited to what the policy and the component's contract allow; appearance comes from variants. warning

Adoption

UI-101 no-raw-button Use <brok:button> instead of a raw <button>. warning
UI-102 no-raw-controls Use the design-system control instead of a raw <input>, <select>, or <textarea>. warning
UI-103 require-field-wrapper Form controls belong inside <brok:field>, which wires label, description, and error. warning
UI-104 no-mixed-composition A view that uses the Brok API must not mix in raw controls. error
UI-105 no-raw-forms Use <brok:form> instead of a raw <form>. warning
UI-106 no-raw-composite Hand-built markup duplicates a composite Brok already ships (table, tabs, accordion, progress, switch, tooltip, dropdown). Use the matching component. warning

Accessibility

UI-201 require-accessible-label Every control needs a detectable accessible label. error
UI-202 require-icon-label Icon-only controls need an accessible name. error
UI-203 no-modal-bypass Use <brok:dialog> instead of a raw <dialog> or a hand-rolled modal. warning
UI-206 no-invalid-interactive-element Clickable markup must be a native button or link. error
UI-207 require-motion-guard Animation utilities need a reduced-motion guard. warning
UI-208 no-broken-references Static ids must be unique and ARIA references must resolve. error
UI-213 require-loading-announcement Loading state must be announced to assistive technology. warning
UI-214 require-destructive-confirmation Destructive actions need a confirmation step. warning

Livewire

UI-204 livewire-loading Livewire actions need loading state the component exposes. warning
UI-205 livewire-contract A component must be used the way its Livewire behavior contract allows. error
UI-209 require-stable-wire-key wire:key must be a stable identifier, not a loop index. error
UI-210 no-eager-live-model Text inputs with wire:model.live need .debounce, .throttle, or .blur. warning
UI-211 field-binding-match A field wrapper and its control must bind the same name. error
UI-212 no-ignored-livewire-directive A Livewire directive on an element that cannot honor it is ignored at runtime. warning
UI-215 require-async-feedback Custom async actions need disabled and aria-busy bindings. warning
UI-216 require-server-validation Forms need server-side validation wiring. warning
UI-217 require-unsaved-guard Forms with unsaved state need a navigation guard. warning

Boundaries

UI-901 core-package-boundary The runtime package must not contain installable Blade components. error
UI-902 no-product-domain-import Shared UI must not import product domain code. error