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

typescript
1// Radix UI / other libraries
2<Trigger asChild>
3  <button className="custom-button">
4    Click me
5  </button>
6</Trigger>

✅ DinachiUI with Base UI

typescript
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.

typescript
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.

typescript
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.

typescript
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 merged

Render Callback with mergeProps

Using mergeProps inside render callbacks for custom styling

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

css
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.

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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>