macbetter

Get started

Installation

Add macbetter to a React app in four steps. You need React 19 or newer.

Install the packages

macbetter's components wrap ui-sdk's, so install ui-sdk next to it. Installing both packages yourself keeps one copy of ui-sdk's core in your app.

pnpm add macbetter @ui-sdk/react @ui-sdk/core

Import the stylesheet

Import macbetter/styles.css once, in your global stylesheet. Everything in it sits in a cascade layer named macbetter, so any rule you write outside a layer wins without !important.

Declare the layer order at the top of the file. ui-sdk's layer comes first and macbetter's sits above it. If you use Tailwind CSS v4, list Tailwind's layers around them and import Tailwind before macbetter, as this site does:

@layer ui-sdk, theme, base, macbetter, components, utilities;

@import "tailwindcss";
@import "macbetter/styles.css";

With that order, Tailwind's utilities override macbetter's component styles, and macbetter's tokens override Tailwind's preflight. Without Tailwind, the first line is just:

@layer ui-sdk, macbetter;

Set the platform and theme

Render <MacbetterScript /> at the top of <body> in your root layout. It's a small inline script that sets data-platform on <html> before the page paints: macos for a fine pointer, and ios or ipados for touch, split at 744px wide.

Put data-theme on <html> to choose a theme, and data-macbetter on <body> to give the page macbetter's background, text color and font.

import { MacbetterScript } from "macbetter"
import "./globals.css"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" data-theme="ember" suppressHydrationWarning>
      <body data-macbetter="">
        <MacbetterScript />
        {children}
      </body>
    </html>
  )
}

suppressHydrationWarning is there because the script changes an attribute on <html> before React hydrates.

Colors follow the system's light or dark setting. To pin one, add data-scheme="light" or data-scheme="dark" to any element. ui-sdk's createColorScheme writes data-color-scheme, which macbetter reads the same way.

Use a component

Import each component from its own entry.

import { Button } from "macbetter/button"
import { Switch } from "macbetter/switch"

export function Example() {
  return (
    <>
      <label>
        Wi-Fi
        <Switch defaultChecked />
      </label>
      <Button variant="filled">Join network</Button>
    </>
  )
}

Next.js

macbetter ships its TypeScript source rather than compiled JavaScript. Next.js needs to compile it, so add it to transpilePackages:

import type { NextConfig } from "next"

const nextConfig: NextConfig = {
  transpilePackages: ["macbetter"],
}

export default nextConfig

With another bundler, make sure it compiles TypeScript and JSX from dependencies too.

Render elements in client components

Some parts take an element through render, such as a button that renders as a link. Create that element in a client component:

"use client"

import { Button } from "macbetter/button"
import Link from "next/link"

export function DocsLink() {
  return <Button render={<Link href="/docs" />}>Read the docs</Button>
}

An element created in a Server Component can reach the browser before React has resolved it. ui-sdk reads it straight away, so it throws "Element type is invalid". It depends on timing, which makes it easy to miss in development.

Imports

ImportWhat it gives you
macbetter/<component>One component and its parts, like macbetter/segmented-control
macbetterEvery component, plus MacbetterScript, resolvePlatform, cx and the theme helpers
macbetter/styles.cssThe whole stylesheet: tokens, platforms, themes, base, materials and components
macbetter/themes/<name>.cssOne built-in theme's stylesheet on its own
macbetter/themecreateTheme, themeVariables, contrastRatio and their types

Prefer the per-component entries in app code: they make it obvious what a file uses.

MacbetterScript options

PropTypeDefaultDescription
platform"macos" | "ipados" | "ios" | "auto""auto"The platform to render. auto follows the device.
storageKeystring—A localStorage key that holds a saved platform choice, if you let people pick one. The saved value wins over platform.
noncestring—Passed to the script tag for a Content Security Policy.

To resolve the platform in your own code, call resolvePlatform() from macbetter. It applies the same rule as the script.