Styles and components Required
Build each control once
Build each control once in a shared component library, and use it on every screen. Never style a raw button, input, select or table on a page.
How to do it
Put buttons, fields, selects, tables, dialogs, toasts and empty states in one folder or package. Base them on native elements, as native controls explains. Add a lint rule that bans the raw elements outside that folder. For server-rendered apps, do the same with template macros or partials.
Example
// eslint.config.js
import tseslint from 'typescript-eslint';
export default [{
files: ['src/screens/**/*.tsx'],
languageOptions: { parser: tseslint.parser },
rules: {
'no-restricted-syntax': ['error', {
selector: "JSXOpeningElement[name.name=/^(button|input|select|table|dialog)$/]",
message: 'Use the component from src/ui instead of a raw element.',
}],
},
}];
import pathlib
import re
import sys
RAW_CONTROL = re.compile(r"<(button|input|select|table|dialog)\b")
problems = [
str(path) for path in pathlib.Path("templates/screens").rglob("*.html")
if RAW_CONTROL.search(path.read_text())
]
print("\n".join(problems) or "every control comes from templates/ui")
sys.exit(1 if problems else 0)
How to check
The linter fails when a screen outside the library uses <button>, <input>, <select> or <table> directly.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers app interface playbook: https://agentwranglers.se/playbook/app-ui/styles-and-components/shared-components.