Dinachi design conventions
Understand the key conventions and patterns used in DinachiUI to build accessible, consistent and maintainable components.
Why DinachiUI?
DinachiUI is built on top of Base UI, a library of unstyled React UI components that provides complete control over your app's CSS and accessibility features. This foundation gives us powerful features while maintaining flexibility.
Accessibility First
Built-in ARIA attributes and keyboard navigation
Unstyled Foundation
No CSS conflicts, complete style control
Modern React
Hooks-based API with render props
Render props instead of asChild
Instead of the asChild prop pattern, Base UI components use render functions that give you complete control over the rendered output.
❌ Other Libraries
1// Radix UI / other libraries
2<Trigger asChild>
3 <button className="custom-button">
4 Click me
5 </button>
6</Trigger>✅ DinachiUI with Base UI
1// Base UI / DinachiUI
2<Trigger
3 render={(props) => (
4 <button {...props} className="custom-button">
5 Click me
6 </button>
7 )}
8/>Benefits & Trade-offs:
- • More explicit control over props spreading
- • Easier to compose with other patterns and nest components
- • No React.cloneElement usage (more predictable behavior)
- • Slightly more verbose than asChild pattern
- • May be less intuitive for developers new to render props
Building custom components with useRender hook
The useRender hook lets you build custom components that provide a render prop to override the default rendered element.
1import { useRender } from '@base-ui/react/use-render';
2import { mergeProps } from '@base-ui/react/merge-props';
3
4interface TextProps extends useRender.ComponentProps<'p'> {}
5
6function Text(props: TextProps) {
7 const { render, ...otherProps } = props;
8
9 const element = useRender({
10 defaultTagName: 'p',
11 render,
12 props: mergeProps<'p'>({ className: 'text-class' }, otherProps),
13 });
14
15 return element;
16}
17
18// Usage
19<Text>Default paragraph</Text>
20<Text render={<strong />}>Strong text</Text>useRender enables:
- • Render prop pattern for custom components
- • Automatic prop merging and spreading
- • TypeScript support with proper inference
- • State passing through render callbacks
Render Function with State
Pass component state through render callbacks
The callback version of render prop provides access to internal component state.
1interface CounterState {
2 odd: boolean;
3}
4
5interface CounterProps extends useRender.ComponentProps<'button', CounterState> {}
6
7function Counter(props: CounterProps) {
8 const { render = <button />, ...otherProps } = props;
9 const [count, setCount] = React.useState(0);
10
11 const odd = count % 2 === 1;
12 const state = React.useMemo(() => ({ odd }), [odd]);
13
14 const defaultProps: useRender.ElementProps<'button'> = {
15 type: 'button',
16 children: <>Counter: <span>{count}</span></>,
17 onClick() { setCount(prev => prev + 1); },
18 };
19
20 const element = useRender({
21 render,
22 state,
23 props: mergeProps<'button'>(defaultProps, otherProps),
24 });
25
26 return element;
27}
28
29// Usage with state access
30<Counter
31 render={(props, state) => (
32 <button {...props}>
33 {props.children}
34 <span>{state.odd ? '👎' : '👍'}</span>
35 </button>
36 )}
37/>Prop Merging with mergeProps
mergeProps Function
Safely merge React props including event handlers, className, and styles
The mergeProps function merges two or more sets of React props together, safely handling event handlers, className strings, and style properties.
1import { mergeProps } from '@base-ui/react/merge-props';
2
3// Basic prop merging
4function Button({ render = <button />, ...props }) {
5 const defaultProps = {
6 className: 'btn-default',
7 onClick: () => console.log('default click'),
8 style: { padding: '8px' }
9 };
10
11 return useRender({
12 render,
13 props: mergeProps<'button'>(defaultProps, props)
14 });
15}
16
17// All props are safely merged:
18// - Event handlers: both onClick functions will be called
19// - className: strings are concatenated
20// - style: objects are mergedRender Callback with mergeProps
Using mergeProps inside render callbacks for custom styling
1import { mergeProps } from '@base-ui/react/merge-props';
2
3function CustomButton() {
4 return (
5 <Button
6 render={(props, state) => (
7 <button
8 {...mergeProps<'button'>(props, {
9 className: 'custom-button',
10 style: {
11 backgroundColor: state.pressed ? 'blue' : 'gray'
12 }
13 })}
14 >
15 {props.children}
16 </button>
17 )}
18 >
19 Click me
20 </Button>
21 );
22}Component Composition
Custom Component Composition
Composing Base UI parts with custom React components
Use the render prop to compose Base UI components with your own custom components.
1// Composing with custom components
2<Menu.Trigger render={<MyButton size="md" />}>
3 Open menu
4</Menu.Trigger>
5
6// MyButton must forward ref and spread props
7const MyButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
8 function MyButton({ size = 'md', ...props }, ref) {
9 return (
10 <button
11 ref={ref}
12 {...props}
13 className={cn('my-button', `size-${size}`, props.className)}
14 />
15 );
16 }
17);Multiple Component Composition
Nesting render props for complex component interactions
Render props can be nested deeply for complex component combinations like Tooltip + Dialog + Menu.
1// Complex nested composition
2<Dialog.Root>
3 <Tooltip.Root>
4 <Tooltip.Trigger
5 render={
6 <Dialog.Trigger
7 render={
8 <Menu.Trigger render={<MyButton size="md" />}>
9 Open menu
10 </Menu.Trigger>
11 }
12 />
13 }
14 />
15 <Tooltip.Portal>
16 <Tooltip.Content>Opens a dialog with menu</Tooltip.Content>
17 </Tooltip.Portal>
18 </Tooltip.Root>
19 <Dialog.Portal>
20 <Dialog.Content>...</Dialog.Content>
21 </Dialog.Portal>
22</Dialog.Root>Changing Default Elements
Override the default rendered element using render props
You can use render props to change the underlying HTML element, like rendering a Menu.Item as an anchor tag.
1// Rendering Menu.Item as a link
2<Menu.Root>
3 <Menu.Trigger>Song</Menu.Trigger>
4 <Menu.Portal>
5 <Menu.Positioner>
6 <Menu.Popup>
7 <Menu.Item render={<a href="https://base-ui.com" />}>
8 Visit Base UI
9 </Menu.Item>
10 <Menu.Item render={<a href="/library" />}>
11 Add to Library
12 </Menu.Item>
13 </Menu.Popup>
14 </Menu.Positioner>
15 </Menu.Portal>
16</Menu.Root>Styling Conventions
Class Variance Authority
Consistent variant system across all components
We use CVA (Class Variance Authority) to create type-safe, consistent variant systems.
1import { cva } from 'class-variance-authority';
2
3const buttonVariants = cva(
4 // Base styles
5 "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
6 {
7 variants: {
8 variant: {
9 default: "bg-primary text-primary-foreground hover:bg-primary/90",
10 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
11 outline: "border border-input bg-background hover:bg-accent",
12 ghost: "hover:bg-accent hover:text-accent-foreground",
13 },
14 size: {
15 default: "h-10 px-4 py-2",
16 sm: "h-9 rounded-md px-3",
17 lg: "h-11 rounded-md px-8",
18 icon: "h-10 w-10",
19 },
20 },
21 defaultVariants: {
22 variant: "default",
23 size: "default",
24 },
25 }
26);CSS Custom Properties
Theme-aware styling with CSS variables
All colors and spacing use CSS custom properties for easy theming and dark mode support.
1:root {
2 --background: 0 0% 100%;
3 --foreground: 222.2 84% 4.9%;
4 --primary: 222.2 47.4% 11.2%;
5 --primary-foreground: 210 40% 98%;
6 --secondary: 210 40% 96%;
7 --secondary-foreground: 222.2 84% 4.9%;
8 --accent: 210 40% 96%;
9 --accent-foreground: 222.2 84% 4.9%;
10 --destructive: 0 84.2% 60.2%;
11 --destructive-foreground: 210 40% 98%;
12}
13
14.dark {
15 --background: 222.2 84% 4.9%;
16 --foreground: 210 40% 98%;
17 /* ... other dark mode variables */
18}Best Practices
Component Composition
How to properly compose Base UI components
✅ Do
- • Use render props for custom markup
- • Spread props correctly to maintain accessibility
- • Follow the established variant patterns
- • Use CSS custom properties for theming
❌ Don't
- • Override internal component structure
- • Skip prop spreading in render functions
- • Use hardcoded colors instead of CSS variables
- • Mix asChild patterns with render props
TypeScript Integration
Getting the most out of TypeScript with Base UI
Base UI provides excellent TypeScript support with proper inference for render props and component props.
1// Type-safe render prop usage
2interface CustomButtonProps {
3 variant?: 'default' | 'destructive' | 'outline';
4 children: React.ReactNode;
5}
6
7function CustomButton({ variant = 'default', children }: CustomButtonProps) {
8 return (
9 <Button
10 render={(props) => (
11 <button
12 {...props} // Properly typed with all button attributes
13 className={cn(buttonVariants({ variant }), props.className)}
14 >
15 {children}
16 </button>
17 )}
18 />
19 );
20}Migration from Other Libraries
Common Migration Patterns
How to adapt from other component libraries
From Radix UI:
❌ Other Libraries
1// Radix UI
2<Dialog.Root>
3 <Dialog.Trigger asChild>
4 <Button>Open Dialog</Button>
5 </Dialog.Trigger>
6 <Dialog.Portal>
7 <Dialog.Overlay />
8 <Dialog.Content>
9 <Dialog.Title>Title</Dialog.Title>
10 <Dialog.Close asChild>
11 <Button>Close</Button>
12 </Dialog.Close>
13 </Dialog.Content>
14 </Dialog.Portal>
15</Dialog.Root>✅ DinachiUI with Base UI
1// DinachiUI with Base UI
2<Dialog>
3 <DialogTrigger
4 render={(props) => (
5 <Button {...props}>Open Dialog</Button>
6 )}
7 />
8 <DialogPortal>
9 <DialogOverlay />
10 <DialogContent>
11 <DialogTitle>Title</DialogTitle>
12 <DialogClose
13 render={(props) => (
14 <Button {...props}>Close</Button>
15 )}
16 />
17 </DialogContent>
18 </DialogPortal>
19</Dialog>From Headless UI:
❌ Other Libraries
1// Headless UI
2<Menu>
3 <Menu.Button>Options</Menu.Button>
4 <Menu.Items>
5 <Menu.Item>
6 {({ active }) => (
7 <a className={active ? 'active' : ''}>
8 Account
9 </a>
10 )}
11 </Menu.Item>
12 </Menu.Items>
13</Menu>✅ DinachiUI with Base UI
1// DinachiUI with Base UI
2<Menu>
3 <MenuTrigger
4 render={(props) => (
5 <Button {...props}>Options</Button>
6 )}
7 />
8 <MenuPositioner>
9 <MenuPopup>
10 <MenuItem
11 render={(props) => (
12 <a {...props} className={cn('menu-item', props.className)}>
13 Account
14 </a>
15 )}
16 />
17 </MenuPopup>
18 </MenuPositioner>
19</Menu>