FUI1008: Component owns caller styling
moderateA caller changed a CSS property the approved component reserves for its own props.
Details
{
"code": "FUI1008",
"ruleId": "components/no-restyle",
"category": "Canonical Usage",
"defaultSeverity": "moderate",
"lifecycle": "experimental",
"fixAvailable": false,
"evidenceRequired": true
}Guidance
## What it means
The approved contract records a `style.ownership` policy for this component: a list of appearance categories the component owns, such as `padding`, `background` or `radius`. Your call site set a CSS property in one of those categories through `className` or `style`, and that exact property is not listed in the record's `allowProperties`.
The finding names the component, the owned categories, the exact properties your code changes, the record's own `because` sentence, and the approved props to use instead. Its severity is the record's `severity`, not a global default.
Categories the record does not own stay yours. If the record owns padding, background and radius, then margin, width and layout utilities still pass on the same element.
## Why it matters
A valid design token is not permission to restyle a component. Padding, color and radius are part of the component's public behavior: they carry its states, its density and its accessibility contrast. When each call site sets them directly, the component's props stop describing what users actually see, and a fix inside the component no longer reaches those screens.
## Before
`Button` owns padding, background and radius, so this call site changes three owned properties:
```tsx import { Button } from "@/components/ui/button";
export function SaveAction() { return <Button className="p-4 rounded-lg bg-primary">Save changes</Button>; } ```
## Repair
Use the props the finding names. The approved alternatives come from the record, so they are the component's real API, not a guess:
```tsx import { Button } from "@/components/ui/button";
export function SaveAction() { return ( <Button size="lg" variant="primary" className="mt-4 w-full"> Save changes </Button> ); } ```
`mt-4` and `w-full` survive: margin and width are outside the owned categories in this example. Read your own finding for the categories your record owns.
Inline styles are read the same way. `style={{ padding: 16 }}` sets an owned property whether the value is a raw number or a token reference.
## Where the policy comes from
Ownership travels with the approved component policy — the frozen record attached to the component in your contract — and it is compiled into the scan with the rest of that component's governance:
```json { "kind": "style.ownership", "owns": ["padding", "background", "radius"], "because": "Use the approved size and variant props.", "severity": "error", "alternatives": [ { "categories": ["padding"], "prop": "size", "values": ["sm", "lg"] }, { "categories": ["background"], "prop": "variant", "values": ["primary", "secondary"] } ] } ```
A component has at most one effective ownership record. Two conflicting records are invalid policy: the check reports the authority as unavailable rather than picking one.
## Author a local component policy
For a design system developed in this repository, put ownership on the component's fragment rather than in `govern.components` or a global config override. For example, alongside a `Button` whose real API includes `size="sm" | "lg"`:
```ts // src/ui/Button.fragment.ts import { defineFragment } from "@usefragments/core"; import { Button } from "./Button";
export default defineFragment({ component: Button, meta: { name: "Button", description: "An action button", category: "forms" }, guidance: { when: ["Actions"], whenNot: [] }, govern: (g) => [ g.styleOwnership({ owns: ["padding"], because: "Use the size prop for button padding.", alternatives: [{ categories: ["padding"], prop: "size", values: ["sm", "lg"] }], }), ], }); ```
`styleOwnership` defaults to `severity: "error"`. Supply only alternatives your component actually supports; the builder does not infer prop values. A literal `governance` array with the same `style.ownership` record is also supported. Use one ownership record per component.
Include the definition and the caller source in the local configuration:
```ts // fragments.config.ts export default { include: ["src/ui/**/*.fragment.ts"], components: ["src/**/*.tsx"], app: { include: ["src/App.tsx"] }, govern: { canonicalSources: [{ kind: "directory", path: "src/ui", include: ["Button"] }], rules: { "components/no-restyle": { enabled: true, severity: "error" }, "components/unresolved-style": { enabled: true, severity: "error" }, }, }, }; ```
Run `npx @usefragments/cli build`, then `npx @usefragments/cli check --ci`. An imported `<Button style={{ padding: 16 }} />` violates this policy; `<Button size="lg" />` uses its public API. An unowned margin remains allowed. The same builder is available in the `govern` callback of the two-argument `defineFragment(Button, definition)` format.
This authors **local development policy**. In a connected check, the captured approved component policy is authoritative. Editing a local fragment or rebuilding `fragments.json` does not change that frozen approval or grant a new exception; update the contract through its approval flow.
## Intentional exception
Use a line-attached exception only when an owner has accepted the override and is removing it:
```tsx // @fragments-expect-error FUI1008 reason="marketing hero spacing tracked in DS-704" expires="2026-12-31" const heroAction = <Button className="p-8">Start free trial</Button>; ```
Do not add a repository-wide exclude. Review active exceptions with `npx @usefragments/cli check --list-suppressions` and fail expired ones with `npx @usefragments/cli check --check-expired`.
## Verify the change
Run `npx @usefragments/cli check --changed --format agent` and confirm FUI1008 is absent or lists only the reviewed exception. Then run `npx @usefragments/cli check --changed --ci`.
Intentional exception? Use a narrow, reasoned directive from the in-source exception reference.
Next steps
- RulesThe rules that emit FUI codes.
- ExceptionsIn-source allows and Cloud exceptions.