@mixedpplparty/juicer-m3
v1.1.12
Published
Accessible Material 3 Expressive React components built with Base UI.
Readme
juicer-m3
Accessible Material 3 Expressive components for React 18 and 19, built with
Base UI and Motion. The visual system follows the m3 juicer Figma
library and is implemented with application-overridable CSS custom properties.
Install
pnpm add juicer-m3React and React DOM are peer dependencies. Load the complete stylesheet once:
import "juicer-m3/styles.css";
import { Button, ThemeProvider } from "juicer-m3";
export function App() {
return (
<ThemeProvider theme="light">
<Button variant="filled">Continue</Button>
</ThemeProvider>
);
}styles.css imports tokens.css and components.css. Applications that need explicit control
may load the two files separately, with tokens first. Package declarations live in low-priority
jm3.tokens and jm3.components cascade layers. Normal unlayered application CSS therefore wins
without !important.
Theme overrides
All component colors, state layers, focus rings, icons, disabled states, error states, and
elevations resolve through CSS variables. ThemeProvider uses local scope by default, preserving
the existing subtree behavior and routing portaled components into the themed subtree:
<ThemeProvider theme="teal-dark">
<AdminArea />
</ThemeProvider>Use document scope when the theme should also cover global styles and portals mounted under
document.body:
<ThemeProvider scope="document" theme="dark">
<App />
</ThemeProvider>The "system" preference resolves to the base light or dark theme and updates when the operating
system preference changes:
<ThemeProvider scope="document" theme="system">
<App />
</ThemeProvider>Server rendering resolves "system" to light until the client preference is available. Apps that
require a no-flash first paint should set data-jm3-theme before hydration.
The typed tokens property overrides semantic variables for one provider. In document scope the
same overrides are synchronized to the document root and restored when the provider unmounts:
<ThemeProvider theme="dark" tokens={{ primary: "rebeccapurple" }}>
<AdminArea />
</ThemeProvider>An explicit Select portalProps.container takes precedence over the provider's local portal
container.
CSS-only theming works by setting the same attribute:
<main data-jm3-theme="teal-dark">...</main>The 32 modes are light/dark/high-contrast/medium-contrast, monochrome light/dark, and light/dark
variants for pink, rose, red, orange, yellow, chartreuse, green, teal, cyan, blue, indigo, and
purple. The warm light mode is the default. Every semantic color is an exact resolved value from
the M3 variable collection in the
m3-juicer Figma file;
the package does not approximate palette modes by mixing seed colors.
The checked-in Figma snapshot is scripts/m3-colors.figma.json. After intentionally refreshing
that snapshot from the M3 collection, regenerate and verify the CSS with:
pnpm generate:colors
pnpm check:tokensFonts
The default typeface variables reference IBM Plex Sans KR, but the package does not fetch or bundle fonts. Load the fonts in your application, or replace the variables:
:root {
--md-ref-typeface-brand: "Your Brand Font", system-ui, sans-serif;
--md-ref-typeface-plain: "Your UI Font", system-ui, sans-serif;
}Typography
Text implements the complete m3 juicer type scale: display, headline, title, body, and label
roles in large, medium, and small sizes, with optional emphasized styles. Font size, line height,
weight, family, and tracking are application-overridable CSS variables. All text sizes use rem.
The visual type role is intentionally named typeRole so the native ARIA role attribute remains
available. Text renders a neutral span by default because a visual Material role cannot
determine the surrounding document's heading hierarchy:
import { Text } from "juicer-m3/text";
<Text as="h1" typeRole="display" size="large">
Account overview
</Text>
<Text as="h2" typeRole="headline" size="medium" emphasized>
Recent activity
</Text>
<Text as="p" typeRole="body" size="large">
Activity from the last 30 days.
</Text>Choose h1 through h6 according to the document outline, not the visual size. Avoid skipping
heading levels. Use as="p" for standalone paragraphs and the default span for text embedded
inside another component. The visual label role does not imply an HTML label; use a real
label only when it is associated with a form control. Likewise, emphasized changes the visual
type tokens but does not add semantic stress emphasis.
The root export is also available:
import { Text } from "juicer-m3";Direct imports and icons
Every component family and icon has a tree-shakeable subpath:
import { Button } from "juicer-m3/button";
import { SearchIcon } from "juicer-m3/icons/search";
import "juicer-m3/styles.css";Icon components use normalized Material Symbols Rounded paths, inherit currentColor, are hidden
from assistive technology by default, and become named images when given a title. Icon-only
controls require an aria-label.
Select
Select is a compound, Base UI-backed control with outlined and filled Material variants.
Its label is associated with the trigger automatically, and keyboard navigation, typeahead,
form submission, focus management, disabled options, groups, portals, and dismissal follow
Base UI's accessible Select behavior:
import { Select } from "juicer-m3/select";
const fruits = [
{ label: "Apple", value: "apple" },
{ label: "Orange", value: "orange" },
];
<Select.Root defaultValue="orange" items={fruits} name="fruit">
<Select.Label>Fruit</Select.Label>
<Select.Trigger>
<Select.Value placeholder="Choose a fruit" />
<Select.Icon />
</Select.Trigger>
<Select.Description>Choose one option.</Select.Description>
<Select.Popup>
<Select.List>
{fruits.map((fruit) => (
<Select.Item key={fruit.value} value={fruit.value}>
<Select.ItemIndicator />
<Select.ItemText>{fruit.label}</Select.ItemText>
</Select.Item>
))}
</Select.List>
</Select.Popup>
</Select.Root>;Set variant="filled" on Select.Root for the filled text-field treatment. Pass validation
state through fieldProps, for example fieldProps={{ invalid: true }}, and render
Select.Error for the associated error message. Select.Group, Select.GroupLabel,
Select.Separator, and the scroll-arrow parts support longer structured menus. For large or
filterable datasets, use a combobox instead of Select so people can search the available values.
className and CSS-in-JS generated classes on Select.Root are applied to the
.jm3-select-field wrapper, so styles such as inline-size: 100% control the complete field.
Skeleton
Skeleton follows the shadcn Base UI component contract: it renders a plain <div> with
data-slot="skeleton", merges className, forwards native div props, and leaves its dimensions
to application classes or styles.
import { Skeleton } from "juicer-m3/skeleton";
<Skeleton className="profile-placeholder" />;.profile-placeholder {
inline-size: 6.25rem;
block-size: 1.25rem;
border-radius: var(--md-sys-shape-corner-full);
}Its fill and default radius use Material CSS variables, and its pulse animation is disabled when
the user requests reduced motion. Skeletons are visual placeholders rather than progress
announcements. Put aria-busy="true" and an accessible name on the loading region when status
must be communicated, and mark decorative skeleton shapes aria-hidden="true".
Avatar
Avatar follows the shadcn Base UI API, including image fallback behavior, badges, groups, group
counts, and sm, default, and lg sizes:
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "juicer-m3/avatar";
<AvatarGroup aria-label="Project members">
<Avatar>
<AvatarImage alt="Chris Nguyen" src="/avatars/chris.jpg" />
<AvatarFallback>CN</AvatarFallback>
<AvatarBadge aria-label="Online" />
</Avatar>
<Avatar>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<AvatarGroupCount aria-label="3 more members">+3</AvatarGroupCount>
</AvatarGroup>;Always include AvatarFallback, provide useful alt text for meaningful profile images, and
label an empty AvatarBadge when it communicates status. Sizes, overlap, badges, and typography
use rem-based values; colors, outlines, and rings use application-overridable Material CSS
variables.
Role indicators
RoleIndicator pairs an application-supplied role color with a readable role name. The marker is
decorative—the text carries the meaning—so role information remains available without relying on
color alone. The active state adds the Figma-specified inset underline without changing layout.
import { RoleIndicator } from "juicer-m3/role-indicator";
<RoleIndicator
active
color="var(--app-role-admin)"
roleName="Admin"
/>;The label uses the shared M3 body-medium typography variables. The active underline uses
--md-sys-color-surface-tint-opacity-16, and both it and the role color can be overridden with
CSS variables.
Composable list items
ListItem renders a semantic <li> by default. Use its Base UI-style render prop when the
whole visual row should be an anchor or a routing-library link:
import { List, ListItem } from "juicer-m3/list";
<List>
<ListItem
headline="Settings"
supportingText="Preferences and account"
render={<a href="/settings" />}
/>
</List>;The rendered link remains inside an <li>, preserving valid list semantics. Event handlers,
classes, refs, ARIA attributes, content, and selected or segmented state are merged into the
rendered element. The callback form receives the merged element props and ListItemState.
Transparent app bars and lists
Use container="transparent" when an app bar or list should inherit the background of its
surroundings:
import { AppBar, BottomAppBar } from "juicer-m3/app-bar";
import { List } from "juicer-m3/list";
<AppBar container="transparent" title="Dashboard" />;
<List container="transparent">{/* list items */}</List>;
<BottomAppBar container="transparent">{/* actions */}</BottomAppBar>;The default value is container="default". A transparent app bar also suppresses elevation,
including when elevated is present. Selected list items and hover, focus, and pressed state layers
remain visible inside a transparent list.
SSR
Modules are ESM and safe to import during server rendering. Interactive entry points carry
"use client"; presentational modules do not. In React Server Component applications, place
interactive controls below a client boundary. Load CSS from the application shell to avoid a
flash of unstyled content.
Local package distribution with pnpm
The package can be installed directly from a generated tarball; publishing to npm is not
required. A tarball is preferable to pnpm link when validating a release because it contains
exactly the files that a published package would contain and exercises the declared package
exports.
From this repository, install dependencies and create the package:
cd C:\code-repos\juicer-m3
pnpm install
pnpm packThe prepack script builds the ESM modules and declarations and validates the package exports.
The resulting file is written to the repository root and includes the package version in its
name, for example:
C:\code-repos\juicer-m3\juicer-m3-0.1.3.tgzTo place the exported package in a different directory, pass an explicit destination:
pnpm pack --pack-destination "C:\local-packages"In the project that will consume the library, install the tarball by its path:
cd C:\code-repos\my-react-app
pnpm add "C:\code-repos\juicer-m3\juicer-m3-0.1.3.tgz"The tarball can also be copied into the consuming repository—for example, into a vendor
directory—and installed with a relative path:
pnpm add ".\vendor\juicer-m3-0.1.3.tgz"pnpm records the local tarball as a file: dependency in the consuming project's
package.json and lockfile. The consuming application uses the package exactly like an npm-hosted
package:
import { Button, ThemeProvider } from "juicer-m3";
import { SearchIcon } from "juicer-m3/icons/search";
import "juicer-m3/styles.css";React and React DOM remain peer dependencies and should already be dependencies of the consuming application. The application is also responsible for loading IBM Plex Sans KR or overriding the font variables.
After changing juicer-m3, run pnpm pack again. Prefer incrementing the package version before
sharing a new artifact so its filename and lockfile entry are unambiguous. For quick local
iteration with the same version, reinstall the rebuilt tarball with pnpm add --force <path-to-tarball>.
Treat copied tarballs as immutable build artifacts and do not edit their contents manually.
Development
pnpm storybook
pnpm test
pnpm test:e2e
pnpm check
pnpm packBiome is the only formatter and linter. Vitest covers semantics, Base UI interaction, form state,
focus restoration, theming, SSR, and color-token invariants. Playwright targets Chromium, Firefox,
and WebKit. pnpm pack writes a publication-ready tarball to the repository root.
License
MIT. Material Symbols are licensed under Apache-2.0; see THIRD_PARTY_NOTICES.md.
