@021.is/brand-studio
v0.6.1
Published
Generic MD3 brand-kit toolkit: seed → full light + dark role set, composable SVG shapes + motion + loading marks, mountable <BrandStudio> /brand page, mandatory icon + logo download-URL contract. Ships no brand content — every app supplies its own logo, i
Downloads
0
Maintainers
Readme
Why
Brand work in a React app usually splits between hand-authored hex palettes that drift across light + dark, hand-drawn marks that get pasted into every product, and a /brand page that nobody maintains. brand-studio replaces all three with one toolkit:
- Seed in, full scheme out. One hex seed produces a contrast-correct MD3 role set for both light and dark via Google's
@material/material-color-utilities(HCT). - Composable SVG primitives.
Squircle,Ring,Disc, plus generic loading marks and motion helpers. Everything paints fromcurrentColoror the MD3 vars the toolkit installs, so consumer-side theming is one line. - A mountable
/brandpage. Drop<BrandStudio>on a catch-all route and you get a live brand kit (overview · logo · colors · type · icons · motion · download) themed from your seed. - Brand-content free. No logos, no icon catalog, no hardcoded palette. Pass your own marks via
staticLogo/animatedLogo/icons.
Install
bun add @021.is/brand-studio
# or
npm install @021.is/brand-studioPeers: react ^19, react-dom ^19, framer-motion ^11 || ^12.
Declare your brand
// brand.config.ts
import { defineBrand } from "@021.is/brand-studio/define";
export default defineBrand({
name: "Acme",
tagline: "We make things.",
color: {
seed: "#3B82F6", // required — the MD3 scheme derives from this
tertiary: "#22D3EE", // optional accent
// secondary, neutral, contrastLevel (-1..1), variant, overrides also supported
},
type: { sans: "Inter", mono: "JetBrains Mono" },
voice: ["Plain. Not corporate.", "Flat price. No surprises."],
});defineBrand eagerly resolves config.scheme (the full { light, dark } role set) so downstream consumers never wait on a derivation.
Expose the download matrix (mandatory, v0.4+)
Every consumer ships a /brand/download link grid for the app icon and logo at multiple sizes in both light and dark variants. Wire BrandConfig.downloads with buildStandardDownloads(basePath) and serve the URLs from your host app:
// brand.config.ts
import { defineBrand, buildStandardDownloads } from "@021.is/brand-studio/define";
export default defineBrand({
name: "Acme",
// ... color, type, voice ...
downloads: buildStandardDownloads({ basePath: "/brand/download" }),
});Default URL shape: <basePath>/<kind>/<size>/<mode>.<format>
Example: /brand/download/icon/256/light.svg · /brand/download/logo/64/dark.svg
Canonical matrix:
- Icon sizes: 64 / 128 / 256 / 512 / 1024 (square edge px)
- Logo sizes: 32 / 48 / 64 / 96 / 128 (height px)
- Each size in both
lightanddark - Format:
svgby default (PNG opt-in via theformatsoption)
The host app serves each URL — typically one Next.js route handler that renders the SVG mark sized + recoloured per the path params. Consumers that don't wire downloads keep working: the Download section falls back to the v0.3.x on-click SVG-blob generator, with a banner pointing at this section.
Mount the brand page
// app/brand/[[...path]]/page.tsx
import config from "@/../brand.config";
import { BrandStudio } from "@021.is/brand-studio";
import { MyLogo, MyAnimatedMark } from "@/brand/Logo";
export default async function BrandPage({ params }: { params: Promise<{ path?: string[] }> }) {
const { path = [] } = await params;
return (
<BrandStudio
config={config}
route={path}
basePath="/brand"
staticLogo={<MyLogo />}
animatedLogo={<MyAnimatedMark />}
icons={[{ node: <MyGlyph />, label: "glyph" }]}
/>
);
}<BrandStudio> injects the MD3 role set as --md-* CSS vars (light by default, dark under an ancestor .dark) and themes itself from them — contrast guaranteed in both modes without writing CSS.
Use the colour engine directly
import { generateScheme, schemeToCssText } from "@021.is/brand-studio/color";
const scheme = generateScheme({ seed: "#2EE5A8", tertiary: "#94B5F7" });
// scheme.light.primary, scheme.dark.onSurface, ...
const css = schemeToCssText(scheme);
// ":root { --md-primary: ... } .dark { --md-primary: ... }"The engine supports secondary, tertiary, neutral, neutralVariant, error, contrastLevel (-1.0..1.0), variant (tonalSpot / vibrant / expressive / neutral / monochrome / fidelity / content), and an overrides map for any role you want to pin literally.
Exports
| Subpath | What's in it |
| --- | --- |
| . | Everything — BrandStudio, defineBrand, shapes, motion, loading marks |
| /color | generateScheme, schemeToCssText, role types, contrast helpers |
| /shapes | Squircle, Ring, Disc |
| /motion | Sweep, Drift, PulseLoop, RingDraw, Wobble, AppearGrow, Sequence, Strikethrough |
| /loading | SpinnerLoadingMark, RingLoadingMark, PulseLoadingMark, LoadingMark dispatcher |
| /app | <BrandStudio> + sections (when you want them standalone) |
| /define | defineBrand, BrandConfig type |
Philosophy
- Colour from a seed, not hand-picked hex. Hex defaults invite drift. The MD3 engine derives the entire palette, and contrast is part of the contract.
- Each project owns its logo and icons. Passed as
ReactNodeprops. - Zero commercial runtime deps. Framer Motion (MIT) as peer;
@material/material-color-utilities(Apache-2.0) bundled. No SaaS, no editor lock-in. - SSR-safe and Tailwind-agnostic. Inline styles + CSS vars; never a framework class name.
License
MIT © edvone
