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/coreImport 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 nextConfigWith 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
| Import | What it gives you |
|---|---|
macbetter/<component> | One component and its parts, like macbetter/segmented-control |
macbetter | Every component, plus MacbetterScript, resolvePlatform, cx and the theme helpers |
macbetter/styles.css | The whole stylesheet: tokens, platforms, themes, base, materials and components |
macbetter/themes/<name>.css | One built-in theme's stylesheet on its own |
macbetter/theme | createTheme, themeVariables, contrastRatio and their types |
Prefer the per-component entries in app code: they make it obvious what a file uses.
MacbetterScript options
| Prop | Type | Default | Description |
|---|---|---|---|
platform | "macos" | "ipados" | "ios" | "auto" | "auto" | The platform to render. auto follows the device. |
storageKey | string | — | A localStorage key that holds a saved platform choice, if you let people pick one. The saved value wins over platform. |
nonce | string | — | 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.