Lint 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.
Two layers
The registry contract is always on: a class in a utility family the component's recipe owns
(the button's padding and height belong to size) is reported with the prop
that owns it, and the values that prop accepts. The important form (!px-8)
is the sanctioned escape hatch.
The project policy turns on once you set allow, deny
or contracts. Then every class on a design-system component is checked:
appearance comes from variants, and layout goes where the policy says. Start with layout allowed:
"no-restyle": ["error", { "allow": ["layout"] }]
{{-- Allowed: layout goes on the component, appearance comes from props. --}}
<brok:button class="mt-4 w-full">Save changes</brok:button>
<brok:button size="lg" variant="destructive">Delete account</brok:button>
{{-- Reported: padding, colour and shape are the recipe's to own. --}}
<brok:button class="p-4 bg-pink-500 rounded-full">Save changes</brok:button>
Spacing findings list the component's sizes; other appearance findings list its variants. The linter reads both from the installed contract.
"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.
Contracts
Give components different rules. A contract replaces the keys it writes and inherits the rest; include
layout in a contract's allow when it should keep layout classes.
With deny alone, every class except the denied ones is allowed.
"no-restyle": ["error", {
"allow": ["layout"],
"deny": ["w-*"],
"contracts": [
{ "pattern": "^card\\.title$", "allow": ["layout", "typography"] },
{ "pattern": "^card\\.content$", "allow": ["layout", "spacing"] }
]
}]
{{-- Allowed by the contracts. --}}
<brok:card.title class="text-sm">Account settings</brok:card.title>
<brok:card.content class="p-4">Profile details</brok:card.content>
{{-- Reported: colour is not opened by either contract; width is denied everywhere. --}}
<brok:card.title class="text-pink-500">Account settings</brok:card.title>
<brok:button class="w-full">Save</brok:button>
Your own words
One message for every finding, or one per category (layout, color,
typography, spacing, shape,
effects, motion) with a default.
sizes, variants, component,
category, entries, around and
file are available.
"no-restyle": ["error", {
"allow": ["layout"],
"message": {
"spacing": "Use a {{component}} size: {{sizes|none defined}}.",
"default": "Use a {{component}} variant: {{variants|none defined}}."
}
}]
Limits
- Only installed design-system components are checked. Plain elements and unknown components are outside this rule.
- A class the grammar cannot classify is reported as
unclassified, even with layout allowed. Allow a custom class by name. - Entries match class names and groups, not equivalent CSS effects:
w-*does not cover[width:100%]. - Allowing
p-*also allowsp-[13px]through this rule; no-arbitrary-values still checks the value. - Class values the linter cannot read are require-static-classes findings.
Configure
In the lint.rules block of ui.json.
It accepts allow, deny, contracts, message, include, exclude; see
the policy
for what the entries match.
"no-restyle": ["warning", { "allow": [...], "deny": [...], "contracts": [], "message": "...", "include": ["resources/views/components/ui"], "exclude": ["resources/views/legacy/**"] }]
php artisan ui:check --explain no-restyle
Suppress one line
{{-- ui-lint-disable-next-line no-restyle: reason --}}