Skip to main content

FUI1008: Component owns caller styling

moderate

A caller changed a CSS property the approved component reserves for its own props.

Details

diagnostic
{
  "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.