DEV Community

Cover image for One Design System, Two Runtimes: Sharing Contracts Between React and React Native
Vellira
Vellira

Posted on Originally published at vellira.dev

One Design System, Two Runtimes: Sharing Contracts Between React and React Native

A cross-platform design system sounds simple until the first real component forces the platforms apart.

React and React Native can expose the same product concept — a button, checkbox, input, modal — while relying on very different runtime primitives.

The web has:

  • DOM attributes,
  • anchors,
  • CSS,
  • hover,
  • keyboard behavior.

React Native has:

  • Pressable,
  • native accessibility props,
  • gesture events,
  • native styling.

Trying to hide all of those differences behind one implementation usually makes the abstraction worse.

In Vellira, I have been taking a different approach:

Share the semantic contract, not the runtime implementation.

That distinction has become one of the most useful architectural rules in the project.

The real goal is consistency, not identical code

When building a cross-platform component library, it is easy to treat the problem mainly as code sharing.

If a web button and a native button look similar, why not reuse as much implementation as possible?

Because developers do not primarily consume the internal component tree.

They consume the public API and its behavior.

For a Button, concepts such as these should feel familiar on both platforms:

  • color
  • appearance
  • size
  • shape
  • loading
  • disabled
  • fullWidth
  • iconOnly

A developer should be able to learn those concepts once and carry that knowledge between React and React Native.

The implementation underneath them does not need to be identical.

A more useful definition of parity is:

Cross-platform parity means shared semantics where the platforms agree, plus explicit platform-specific capabilities where they do not.

That is much more durable than forcing two different runtimes through one abstraction.

Put shared semantics in a platform-neutral contract

In Vellira, the common Button vocabulary lives in a shared type package.

A simplified version looks like this:

export type ButtonSize = 'sm' | 'md' | 'lg';

export type ButtonColor =
  | 'primary'
  | 'neutral'
  | 'success'
  | 'warning'
  | 'danger';

export type ButtonAppearance =
  | 'solid'
  | 'outline'
  | 'ghost'
  | 'soft'
  | 'link';

export interface BaseButtonProps {
  color?: ButtonColor;
  appearance?: ButtonAppearance;
  size?: ButtonSize;
  shape?: 'square' | 'rounded' | 'pill';
  fullWidth?: boolean;
  loading?: boolean;
  loadingText?: string;
  disabled?: boolean;
  iconOnly?: boolean;
}
Enter fullscreen mode Exit fullscreen mode

There is nothing specifically web or native in that interface.

It describes what a Vellira Button means.

That shared layer gives both runtimes a stable semantic center.

If appearance="soft" exists on both platforms, it should represent the same design intent.

If loading disables interaction, that rule should remain consistent too.

This is where code sharing provides the most value.

Let each platform extend the contract honestly

Once the shared semantics are defined, each runtime can extend them with the capabilities developers naturally expect.

On the web, a Button can expose native HTML button behavior and anchor-related properties:

export interface ButtonProps
  extends
    BaseButtonProps,
    Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'color'>,
    Pick<
      AnchorHTMLAttributes<HTMLAnchorElement>,
      'href' | 'target' | 'rel' | 'download'
    > {
  children?: ReactNode;
  iconStart?: ReactNode;
  iconEnd?: ReactNode;
  tooltip?: string;
  badge?: ReactNode;
  shortcut?: ReactNode;
  asChild?: boolean;
}
Enter fullscreen mode Exit fullscreen mode

React Native begins with the same shared contract, but its runtime surface is different:

export interface ButtonProps
  extends BaseButtonProps, Omit<PressableProps, /* exclusions */> {
  children?: ReactNode;
  iconStart?: ButtonIconElement;
  iconEnd?: ButtonIconElement;
  iconSize?: number;
  onPress?: (event: GestureResponderEvent) => void;
  style?: StyleProp<ViewStyle>;
  textStyle?: StyleProp<TextStyle>;
  accessibilityLabel?: string;
  testID?: string;
}
Enter fullscreen mode Exit fullscreen mode

The two APIs clearly belong to the same design system.

But they are not artificially identical.

A web developer still gets normal web capabilities.

A React Native developer still gets normal native capabilities.

The design system owns the shared semantics without becoming an abstraction tax on either platform.

Healthy divergence is part of a good API

Some differences should not be normalized away.

A web Button can naturally behave like an anchor.

That is where href, target, or asChild make sense.

React Native does not have an HTML anchor element.

Inventing a fake href prop for native just to make the signatures look symmetrical would create parity on paper while making the API less natural.

The same is true in the other direction.

React Native has concepts such as:

  • StyleProp<ViewStyle>
  • testID
  • gesture events
  • native accessibility properties

Wrapping every one of these in invented cross-platform names would make the library harder to understand and debug.

A useful test is:

Does this property describe product-level component behavior, or the runtime that implements it?

Product-level behavior is a strong candidate for the shared contract.

Runtime behavior usually belongs to the platform package.

Share design decisions separately from implementation

Props are only one layer of cross-platform consistency.

Things like:

  • spacing,
  • radii,
  • typography,
  • colors,
  • interaction states

also need a common source of truth.

Those decisions can live in shared tokens while being consumed differently by CSS and React Native style objects.

The principle is the same:

Share the decision. Adapt the execution.

A shared radius token does not require CSS and React Native styling to work the same way.

It only requires both implementations to resolve the same design intent.

That prevents “cross-platform” from turning into “lowest common denominator.”

Test parity at the contract boundary

Separate implementations introduce another risk: drift.

The web Button might gain a new appearance or loading rule while React Native silently falls behind.

So parity needs explicit validation.

There are two kinds of confidence we care about:

  1. Contract confidence — shared semantics are defined in one authoritative place.
  2. Runtime confidence — each implementation proves that those semantics behave correctly on its own platform.

I prefer this model over a huge shared component with a growing number of platform conditionals.

A shared file can look beautifully DRY while hiding real platform-specific bugs.

Separate runtime tests make the differences visible.

A practical three-layer model

The architecture we are converging on in Vellira can be summarized in three layers.

1. Shared semantics

Define the concepts developers should learn once:

size
appearance
color
loading
disabled
shape
Enter fullscreen mode Exit fullscreen mode

These belong in platform-neutral contracts.

2. Platform API

Extend the shared vocabulary with runtime-native capabilities:

Web:
href
target
DOM attributes
asChild

React Native:
onPress
accessibilityLabel
style
testID
Enter fullscreen mode Exit fullscreen mode

Do not disguise real platform differences.

3. Platform implementation

Use the native tools of each runtime:

Web:
DOM + CSS

React Native:
Pressable + native styles
Enter fullscreen mode Exit fullscreen mode

The implementation can diverge significantly as long as the shared semantics remain trustworthy.

This changes how we design new components

With this architecture, the first question is no longer:

How can we reuse the most code?

Instead, we ask:

  • What behavior is genuinely shared?
  • Which names should mean the same thing everywhere?
  • Which states need equivalent semantics?
  • Which capabilities are platform-specific?
  • What must be validated so parity cannot drift accidentally?

Only after that does implementation reuse become interesting.

Sometimes there is useful shared code below the component layer.

Sometimes two mostly independent implementations are the correct answer.

Both are fine if the public contract remains coherent.

The rule I keep coming back to

Cross-platform design systems do not need maximum code sharing.

They need maximum semantic clarity.

Developers should feel that React and React Native belong to the same system.

They should recognize:

  • the vocabulary,
  • the states,
  • the design decisions,
  • the interaction intent.

But they should not have to give up the strengths of their platform to get that consistency.

For Vellira, that means:

Share contracts aggressively. Share implementations selectively.

It is a smaller abstraction.

But it is a much more durable one.


Vellira is an open-source design system for React and React Native being built in public.

Top comments (0)