mdxui
Guides

Component Design

How to design, wrap, or transform components to the mdxui interface - covers TDD with pixel-perfect designs, wrapping external components, and transforming internal designs to mdxui-compliant types

Component Design

How to create mdxui-compliant components through wrapping external libraries or transforming internal designs.

Core Principle

Templates are opinionated implementations of the abstract mdxui interface.

// Any template exports the standard interface
export { components } from '@mdxui/beacon'    // Site template
export { components } from '@mdxui/cockpit'   // App template
export { components } from '@mdxui/tremor'    // Tremor-based template
export { components } from '@mdxui/custom'    // Your custom template

Same MDX content, completely different rendering. The interface is the contract.

Two Patterns

1. WRAP (External Components)

When: Using external libraries (Tremor, shadcn, Radix, etc.)

Rule: Code and styles do NOT change. Only component names and props change to match mdxui types/schemas.

// DON'T modify Tremor's implementation
// DO wrap it to mdxui interface

// tremor-adapter.tsx
import { Card as TremorCard, Title, Text } from '@tremor/react'
import type { CardProps } from 'mdxui'

export const Card: React.FC<CardProps> = ({
  title,
  description,
  children,
  ...props
}) => (
  <TremorCard {...props}>
    {title && <Title>{title}</Title>}
    {description && <Text>{description}</Text>}
    {children}
  </TremorCard>
)

2. TRANSFORM (Internal Components)

When: Designed components that weren't built mdxui-first

Rule: Migrate to mdxui-compliant types. This is a transformation, not a wrap.

// Before: Original design
export const FeatureSection = ({
  headline,
  items,
  layout
}: BryantFeatureProps) => { ... }

// After: Transform to mdxui interface
export const Features: React.FC<FeaturesProps> = ({
  title,      // was: headline
  features,   // was: items
  columns,    // was: layout
  variant,
}) => {
  // Implementation may change to support mdxui props
  ...
}

Ideal Workflow

Best: Design directly in mdxui components from the start.

Acceptable: Design fast, transform later.

Design Phase (speed priority)     Migration Phase (compliance)
+--------------------------+      +--------------------------+
| Design whatever is       |  ->  | Transform to mdxui       |
| fastest                  |      | types and schemas        |
+--------------------------+      +--------------------------+

If designing in mdxui-style slows down the design process, transformation is an acceptable second step.

TDD with Pixel-Perfect Designs

The Process

1. Start with Design
   - Figma mockup OR
   - External component (Tremor, etc.) OR
   - Existing implementation

2. Create Visual Test
   - Screenshot/visual regression as the "spec"

3. Implement mdxui Interface
   - Define props matching mdxui types
   - Wrap or transform the implementation
   - Verify visual output matches design

4. Test Passes When
   - Visual output is pixel-perfect to original

Visual Regression as TDD

// The design IS the test
describe('Hero', () => {
  it('matches design spec', async () => {
    render(<Hero {...mockProps} />)
    await expect(page).toMatchSnapshot('hero-default')
  })

  it('matches dark mode design', async () => {
    render(<Hero {...mockProps} />, { theme: 'dark' })
    await expect(page).toMatchSnapshot('hero-dark')
  })
})

The mdxui Contract

Every component must satisfy its mdxui type:

import type { HeroProps, FeaturesProps, CardProps } from 'mdxui'
import { HeroPropsSchema } from 'mdxui/zod'

// Type-safe props
export const Hero: React.FC<HeroProps> = (props) => {
  // Runtime validation (optional but recommended)
  const validated = HeroPropsSchema.parse(props)
  return <...>
}

Template Structure

@mdxui/[template-name]/
├── src/
│   ├── components/
│   │   ├── site/           # SiteComponents implementations
│   │   │   ├── Hero.tsx
│   │   │   ├── Features.tsx
│   │   │   └── Pricing.tsx
│   │   ├── app/            # AppComponents implementations
│   │   │   ├── Dashboard.tsx
│   │   │   └── Sidebar.tsx
│   │   └── shared/         # Shared primitives
│   ├── index.ts            # exports { components }
│   └── adapters/           # External library adapters (if wrapping)
│       ├── tremor.tsx
│       └── shadcn.tsx
└── package.json

Export Pattern

// @mdxui/beacon/src/index.ts
import type { SiteComponents } from 'mdxui'

import { Hero } from './components/site/Hero'
import { Features } from './components/site/Features'
import { Pricing } from './components/site/Pricing'
// ... all site components

export const components: SiteComponents = {
  Hero,
  Features,
  Pricing,
  // ... complete interface
}

// Usage in any project
import { components } from '@mdxui/beacon'
// OR
import { components } from '@mdxui/cockpit'
// OR
import { components } from '@mdxui/custom'

// Same MDX, different rendering
<MDXProvider components={components}>
  <Content />
</MDXProvider>

Wrapping External Libraries

Tremor Example

// adapters/tremor/card.tsx
import { Card as TremorCard } from '@tremor/react'
import type { CardProps } from 'mdxui'

// Map mdxui props to Tremor props
export const Card: React.FC<CardProps> = ({
  title,
  description,
  image,
  actions,
  variant,
  children,
}) => (
  <TremorCard
    decoration={variant === 'highlighted' ? 'top' : undefined}
    decorationColor={variant === 'highlighted' ? 'blue' : undefined}
  >
    {image && <img src={image} alt="" />}
    {title && <h3>{title}</h3>}
    {description && <p>{description}</p>}
    {children}
    {actions && <div className="flex gap-2">{actions}</div>}
  </TremorCard>
)

shadcn Example

// adapters/shadcn/button.tsx
import { Button as ShadcnButton } from '@/components/ui/button'
import type { ButtonProps } from 'mdxui'

export const Button: React.FC<ButtonProps> = ({
  label,
  variant = 'default',
  size = 'default',
  icon,
  loading,
  ...props
}) => (
  <ShadcnButton
    variant={variant}
    size={size}
    disabled={loading}
    {...props}
  >
    {loading && <Spinner />}
    {icon && <Icon name={icon} />}
    {label}
  </ShadcnButton>
)

Transforming Internal Components

Before/After Example

// BEFORE: Quick prototype
// components/landing/feature-grid.tsx
export function FeatureGrid({
  headline,
  subhead,
  items,
  cols = 3
}) {
  return (
    <section className="py-24">
      <h2 className="text-4xl font-bold">{headline}</h2>
      <p className="text-xl text-gray-600">{subhead}</p>
      <div className={`grid grid-cols-${cols} gap-8`}>
        {items.map(item => (
          <div key={item.id}>
            <Icon name={item.icon} />
            <h3>{item.title}</h3>
            <p>{item.desc}</p>
          </div>
        ))}
      </div>
    </section>
  )
}

// AFTER: Transformed to mdxui
// components/site/Features.tsx
import type { FeaturesProps, Feature } from 'mdxui'

export const Features: React.FC<FeaturesProps> = ({
  title,        // was: headline
  subtitle,     // was: subhead
  features,     // was: items
  columns = 3,  // was: cols
  variant = 'grid',
}) => {
  return (
    <section className="py-24">
      <h2 className="text-4xl font-bold">{title}</h2>
      {subtitle && <p className="text-xl text-gray-600">{subtitle}</p>}
      <div className={`grid grid-cols-${columns} gap-8`}>
        {features.map((feature: Feature) => (
          <div key={feature.name}>
            {feature.icon && <Icon name={feature.icon} />}
            <h3>{feature.name}</h3>
            {feature.description && <p>{feature.description}</p>}
          </div>
        ))}
      </div>
    </section>
  )
}

Transformation Checklist

  • Props renamed to mdxui standard names
  • Props typed with mdxui interfaces
  • Optional Zod validation added
  • Component exported with mdxui name
  • Visual output unchanged (pixel-perfect)
  • Tests updated to use new props

Quick Reference

ScenarioPatternCode Changes?Style Changes?
External lib (Tremor)WrapNoNo
External lib (shadcn)WrapNoNo
Internal designTransformMaybeNo
New componentBuild mdxui-firstN/AN/A

Anti-Patterns

DON'T:

  • Modify external library source code
  • Change visual design during wrap/transform
  • Skip type compliance for speed
  • Create one-off props that don't match mdxui

DO:

  • Keep external styles intact
  • Map props to mdxui interface
  • Validate with Zod schemas
  • Test for visual regression

On this page