# Material You Theming

> Material You dynamic color — pick a wallpaper and a tonal palette derived from its seed hue re-themes the whole system.

A shadcn/ui block from the [@uicapsule](https://uicapsule.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uicapsule/material-you-theming).

- Page, with a live preview: https://shadcn.exchange/r/uicapsule/material-you-theming
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uicapsule/material-you-theming-light-bdbbca5df68eb445.png
- Registry item: https://uicapsule.com/r/material-you-theming.json
- Categories: effects, colorful
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uicapsule.com/r/material-you-theming.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`

## Files

### `/material-you-theming.tsx`

```tsx
"use client";

import { useState } from "react";
import {
  Bluetooth,
  Flashlight,
  MapPin,
  MessageCircle,
  Moon,
  Music2,
  Plane,
  SkipForward,
  Wifi,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { motion } from "motion/react";

import { PhoneFrame } from "./phone-frame";

interface Wallpaper {
  id: string;
  name: string;
  hue: number;
  hueB: number;
}

const WALLPAPERS: Wallpaper[] = [
  { hue: 200, hueB: 160, id: "lagoon", name: "Lagoon" },
  { hue: 20, hueB: 350, id: "clay", name: "Clay" },
  { hue: 130, hueB: 80, id: "moss", name: "Moss" },
  { hue: 285, hueB: 320, id: "orchid", name: "Orchid" },
];

// A tiny Material-You-style tonal palette derived from the wallpaper seed hue.
const paletteFor = (wallpaper: Wallpaper) => ({
  accent: `hsl(${wallpaper.hueB} 50% 70%)`,
  container: `hsl(${wallpaper.hue} 32% 26%)`,
  muted: `hsl(${wallpaper.hue} 16% 58%)`,
  onContainer: `hsl(${wallpaper.hue} 55% 88%)`,
  onPrimary: `hsl(${wallpaper.hue} 45% 14%)`,
  outline: `hsl(${wallpaper.hue} 18% 32%)`,
  primary: `hsl(${wallpaper.hue} 65% 78%)`,
  surface: `hsl(${wallpaper.hue} 22% 10%)`,
  surfaceHigh: `hsl(${wallpaper.hue} 20% 15%)`,
});

const wallpaperGradient = (wallpaper: Wallpaper) =>
  `linear-gradient(140deg, hsl(${wallpaper.hue} 60% 45%), hsl(${wallpaper.hueB} 55% 30%) 60%, hsl(${wallpaper.hue} 40% 16%))`;

interface QuickTile {
  icon: LucideIcon;
  label: string;
  on: boolean;
}

const QUICK_TILES: QuickTile[] = [
  { icon: Wifi, label: "Internet", on: true },
  { icon: Bluetooth, label: "Bluetooth", on: true },
  { icon: Flashlight, label: "Torch", on: false },
  { icon: Moon, label: "Bedtime", on: false },
  { icon: MapPin, label: "Location", on: true },
  { icon: Plane, label: "Airplane", on: false },
];

const TRANSITION = "background-color 0.7s ease, color 0.7s ease, border-color 0.7s ease";

export const MaterialYouTheming = () => {
  const [active, setActive] = useState<Wallpaper>(
    WALLPAPERS[0] ?? { hue: 200, hueB: 160, id: "", name: "" },
  );
  const palette = paletteFor(active);

  return (
    <PhoneFrame tone="moss">
      <div
        className="absolute inset-0 flex flex-col"
        style={{ backgroundColor: palette.surface, transition: TRANSITION }}
      >
        <motion.div
          key={active.id}
          initial={{ opacity: 0.4 }}
          animate={{ opacity: 1 }}
          transition={{ duration: 0.7 }}
          className="absolute inset-x-0 top-0 h-56"
          style={{
            background: wallpaperGradient(active),
            maskImage: "linear-gradient(to bottom, black 30%, transparent)",
          }}
        />

        <div className="relative px-6 pt-[60px]">
          <p
            className="text-[56px] leading-none font-medium tracking-tight"
            style={{ color: palette.onContainer, transition: TRANSITION }}
          >
            9:41
          </p>
          <p className="mt-1 text-[13px]" style={{ color: palette.muted, transition: TRANSITION }}>
            Mon, Jul 7 · Material You
          </p>
        </div>
        <div className="relative mt-6 grid grid-cols-2 gap-2.5 px-5">
          {QUICK_TILES.map(({ icon: Icon, label, on }) => (
            <div
              key={label}
              className="flex items-center gap-2.5 rounded-[24px] px-4 py-3.5"
              style={{
                backgroundColor: on ? palette.primary : palette.surfaceHigh,
                color: on ? palette.onPrimary : palette.muted,
                transition: TRANSITION,
              }}
            >
              <Icon className="size-4 shrink-0" />
              <span className="truncate text-[12px] font-semibold">{label}</span>
            </div>
          ))}
        </div>
        <div
          className="relative mx-5 mt-2.5 flex items-center gap-3 rounded-[26px] p-4"
          style={{ backgroundColor: palette.container, transition: TRANSITION }}
        >
          <div
            className="grid size-11 shrink-0 place-items-center rounded-2xl"
            style={{
              backgroundColor: palette.primary,
              color: palette.onPrimary,
              transition: TRANSITION,
            }}
          >
            <Music2 className="size-5" />
          </div>
          <div className="min-w-0 flex-1">
            <p
              className="truncate text-[13px] font-semibold"
              style={{ color: palette.onContainer, transition: TRANSITION }}
            >
              Night Drive
            </p>
            <p className="text-[11px]" style={{ color: palette.muted, transition: TRANSITION }}>
              Solenne
            </p>
            <div
              className="mt-2 h-1 w-full overflow-hidden rounded-full"
              style={{ backgroundColor: palette.outline, transition: TRANSITION }}
            >
              <div
                className="h-full w-2/5 rounded-full"
                style={{ backgroundColor: palette.accent, transition: TRANSITION }}
              />
            </div>
          </div>
          <SkipForward className="size-5 shrink-0" style={{ color: palette.muted }} />
        </div>
        <div
          className="relative mx-5 mt-2.5 flex items-center gap-3 rounded-[26px] px-4 py-3.5"
          style={{ backgroundColor: palette.surfaceHigh, transition: TRANSITION }}
        >
          <div
            className="grid size-9 shrink-0 place-items-center rounded-full"
            style={{
              backgroundColor: palette.container,
              color: palette.primary,
              transition: TRANSITION,
            }}
          >
            <MessageCircle className="size-4" />
          </div>
          <div className="min-w-0 flex-1">
            <p
              className="truncate text-[12px] font-semibold"
              style={{ color: palette.onContainer, transition: TRANSITION }}
            >
              Mia
            </p>
            <p
              className="truncate text-[11px]"
              style={{ color: palette.muted, transition: TRANSITION }}
            >
              That new wallpaper looks great on you
            </p>
          </div>
          <span className="text-[10px]" style={{ color: palette.muted, transition: TRANSITION }}>
            now
          </span>
        </div>
        <div className="relative mt-auto pb-8">
          <p
            className="px-6 text-[11px] font-medium"
            style={{ color: palette.muted, transition: TRANSITION }}
          >
            Wallpaper & style
          </p>
          <div className="mt-2.5 flex justify-center gap-3 px-5">
            {WALLPAPERS.map((wallpaper) => (
              <motion.button
                type="button"
                key={wallpaper.id}
                aria-label={`Use ${wallpaper.name} wallpaper`}
                aria-pressed={wallpaper.id === active.id}
                onClick={() => setActive(wallpaper)}
                whileTap={{ scale: 0.92 }}
                animate={{ scale: wallpaper.id === active.id ? 1.06 : 1 }}
                className="relative h-[116px] w-[62px] overflow-hidden rounded-2xl ring-2"
                style={{
                  background: wallpaperGradient(wallpaper),
                  borderColor: "transparent",
                  ...(wallpaper.id === active.id
                    ? { boxShadow: `0 0 0 2px ${palette.surface}, 0 0 0 4px ${palette.primary}` }
                    : { boxShadow: "none" }),
                }}
              >
                <span className="absolute inset-x-0 bottom-1 text-center text-[9px] font-medium text-white/85">
                  {wallpaper.name}
                </span>
              </motion.button>
            ))}
          </div>
          <p
            className="mt-3 text-center text-[11px]"
            style={{ color: palette.muted, transition: TRANSITION }}
          >
            Pick a wallpaper — the whole system re-themes
          </p>
        </div>
      </div>
    </PhoneFrame>
  );
};
```

### `/phone-frame.tsx`

```tsx
import type { ReactNode } from "react";

// Screen is 330×716 (iPhone 15 proportions); the clay shell adds a 16px lip on every side.
export const SCREEN_WIDTH = 330;
export const SCREEN_HEIGHT = 716;

interface Clay {
  base: string;
  deep: string;
  light: string;
  /** RGB triplet for inner shading, tinted so each tone reads as one material. */
  rim: string;
}

// Dark matte clay: `light` is the lit top face, `base` the body, `deep` the shaded underside.
const TONES = {
  espresso: { base: "#3a2b24", deep: "#1f1612", light: "#5a4436", rim: "32 20 14" },
  graphite: { base: "#303036", deep: "#18181c", light: "#4a4a52", rim: "10 10 14" },
  moss: { base: "#2f3a2c", deep: "#171d15", light: "#4a5a44", rim: "12 18 10" },
  ochre: { base: "#4a3c20", deep: "#261e0e", light: "#6e5a30", rim: "30 22 6" },
  plum: { base: "#3c2a3a", deep: "#1e141d", light: "#5c4058", rim: "26 12 24" },
  slate: { base: "#2a3448", deep: "#141a26", light: "#435270", rim: "10 16 30" },
  teal: { base: "#233a3a", deep: "#101d1d", light: "#38585a", rim: "6 22 22" },
  terracotta: { base: "#4a2c22", deep: "#26150f", light: "#6c4232", rim: "34 14 8" },
} satisfies Record<string, Clay>;

export type ClayTone = keyof typeof TONES;

const shellShadow = (rim: string) =>
  [
    "0 60px 100px -40px rgb(0 0 0 / 0.85)",
    "0 26px 40px -24px rgb(0 0 0 / 0.7)",
    "inset 0 6px 6px rgb(255 255 255 / 0.16)",
    `inset 0 -14px 22px rgb(${rim} / 0.75)`,
    "inset 12px 0 18px -12px rgb(255 255 255 / 0.1)",
    `inset -12px 0 18px -12px rgb(${rim} / 0.6)`,
  ].join(", ");

const Nub = ({ className, clay }: { className: string; clay: Clay }) => (
  <span
    aria-hidden="true"
    className={`absolute w-[9px] rounded-full ${className}`}
    style={{
      background: clay.base,
      boxShadow: `inset 2px 2px 2px rgb(255 255 255 / 0.14), inset -2px -2px 3px rgb(${clay.rim} / 0.7), 0 2px 4px rgb(0 0 0 / 0.5)`,
    }}
  />
);

interface PhoneFrameProps {
  children: ReactNode;
  /** Classes for the screen: background, text color, pointer handling. */
  className?: string;
  /** Hide the Dynamic Island when the screen draws its own status area. */
  island?: boolean;
  tone?: ClayTone;
}

export const PhoneFrame = ({
  children,
  className,
  island = true,
  tone = "graphite",
}: PhoneFrameProps) => {
  const clay = TONES[tone];
  return (
    <div
      className="relative shrink-0 rounded-[66px] p-[16px]"
      style={{
        background: `linear-gradient(165deg, ${clay.light} 0%, ${clay.base} 48%, ${clay.deep} 100%)`,
        boxShadow: shellShadow(clay.rim),
      }}
    >
      <Nub clay={clay} className="top-[132px] -left-[6px] h-[36px]" />
      <Nub clay={clay} className="top-[188px] -left-[6px] h-[62px]" />
      <Nub clay={clay} className="top-[262px] -left-[6px] h-[62px]" />
      <Nub clay={clay} className="top-[206px] -right-[6px] h-[92px]" />
      <div
        className={`relative isolate overflow-hidden rounded-[50px] select-none ${className ?? "bg-black"}`}
        style={{
          boxShadow: `0 0 0 2px rgb(${clay.rim} / 0.9), 0 -2px 3px rgb(${clay.rim} / 0.8), 0 3px 4px rgb(255 255 255 / 0.12)`,
          height: SCREEN_HEIGHT,
          width: SCREEN_WIDTH,
        }}
      >
        {children}
        {island && (
          <div
            aria-hidden="true"
            className="pointer-events-none absolute top-[11px] left-1/2 z-50 h-[30px] w-[98px] -translate-x-1/2 rounded-full bg-black"
          />
        )}
      </div>
    </div>
  );
};
```

### `/preview.tsx`

```tsx
"use client";

import type { CSSProperties } from "react";

import { MaterialYouTheming } from "./material-you-theming";

const GRAIN =
  "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.07'/%3E%3C/svg%3E\")";

const backdrop: CSSProperties = {
  background: [
    GRAIN,
    "radial-gradient(ellipse 55% 60% at 30% 18%, rgb(118 128 70 / 0.34), transparent 70%)",
    "radial-gradient(ellipse 50% 55% at 50% 58%, rgb(74 90 52 / 0.3), transparent 75%)",
    "linear-gradient(170deg, #232a17 0%, #161b0f 50%, #0a0d07 100%)",
  ].join(", "),
};

const Preview = () => (
  <main className="flex h-dvh items-center justify-center overflow-hidden p-5" style={backdrop}>
    <MaterialYouTheming />
  </main>
);

export default Preview;
```
