Prop conventions
size="md" has to mean the same thing on a Button, a TextField and a Dialog. The shared vocabulary lives in src/types.ts, and each component takes only the axes it needs. Do not invent a second spelling for an idea that already has one.
The shared types
ts
type NebaSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
type NebaColor = 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info';
type NebaDensity = 'default' | 'compact';
type NebaVariant = 'solid' | 'outline' | 'text';
type NebaElevation = 0 | 1 | 2 | 3;NebaStyleProps bundles the four that most components share.
ts
interface NebaStyleProps {
variant?: NebaVariant; // default 'solid'
size?: NebaSize; // default 'md'
color?: NebaColor; // default 'primary'
density?: NebaDensity; // default 'default'
}A component extends it like this.
ts
export interface ButtonProps
extends NebaStyleProps, Omit<React.ComponentPropsWithoutRef<'button'>, 'color'> {
// props that only this component has
}The Omit<…, 'color'> is there because the native color attribute collides with the name.
What each axis means
| Prop | The rule |
|---|---|
variant | Weight of the surface. One solid per screen (the primary action), outline secondary, text tertiary |
size | The control's height and type scale. See the design language |
color | A semantic role. Arbitrary colour values are not accepted |
density | Padding only. Never the height, never the type scale |
elevation | Drop shadow depth. 0 by default, meaning no shadow at all |
State props
| Prop | Meaning |
|---|---|
disabled | Unavailable. Uses the native disabled attribute |
loading | In progress. Looks unchanged, gets aria-busy, keeps focus |
readOnly | It exists, but not here. Gets aria-disabled, keeps focus |
loading and readOnly deliberately do not use native disabled: dropping out of the focus order costs keyboard users their sense of the page. Activation is stopped in the handler instead.
Naming
- Icon slots are
startIcon/endIcon.leftIcon/rightIconinvert their meaning under RTL. - Booleans are positive.
disabledyes,notDisabledno. - Filling the container is
fullWidth. - Event handlers keep their native names and are passed straight through.
Checklist for a new component
- A
src/components/{lowercase-name}/folder with{PascalCase}.tsxand anindex.tsbarrel - Named exports only (never
export default) - Re-export the barrel from
src/index.ts - Delegate behaviour and accessibility to a Base UI primitive
- Take the axes you need from the shared vocabulary; define only what genuinely has no name yet
test/components/{name}/{Name}.test.tsx— in the same commit- Write
docs/{locale}/components/{group}/{name}.md— intro, preview, props, examples. One page per locale - Add its rows to
docs/.vitepress/data/props.tsand its demos todocs/.vitepress/demos/{name}/ - Give it a card in
demos/gallery/all.tsxand a place ondemos/showcase/app.tsx npm run typecheck && npm test && npm run lintall pass