All components

Book

Book is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.

SmoothUI MIT other

What it is

Book is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i motion

Licence

This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Book

index.tsx
"use client";

import { cn } from "@/lib/utils";
import { useReducedMotion } from "motion/react";
import type { ReactNode } from "react";

export type BookVariant = "stripe" | "simple";

export interface BookProps {
  /** Additional CSS classes */
  className?: string;
  /** Cover accent color (CSS color value) */
  color?: string;
  /** Custom illustration for the stripe area (stripe variant only) */
  illustration?: ReactNode;
  /** Logo or icon displayed at the bottom of the content area */
  logo?: ReactNode;
  /** Title text color override */
  textColor?: string;
  /** Book title text displayed on the cover */
  title: string;
  /** Visual variant of the book */
  variant?: BookVariant;
  /** Book width in pixels */
  width?: number;
}

const BOOK_DEPTH = "29cqw";
const BOOK_BORDER_RADIUS = "6px 4px 4px 6px";
const DEFAULT_WIDTH = 196;
const DEFAULT_COLOR = "#e5a00d";

const BG_SHADOW =
  "linear-gradient(90deg, #fff0 0%, #fff0 12%, #ffffff40 29.25%, #fff0 50.5%, #fff0 75.25%, #ffffff40 91%, #fff0 100%), linear-gradient(90deg, #00000008 0%, #0000001a 12%, #0000 30%, #00000005 50%, #0003 73.5%, #00000080 75.25%, #00000026 85.25%, #0000 100%)";

const Book = ({
  title,
  variant = "stripe",
  color = DEFAULT_COLOR,
  textColor,
  width = DEFAULT_WIDTH,
  illustration,
  logo,
  className,
}: BookProps) => {
  const shouldReduceMotion = useReducedMotion();
  const bookWidth = `${width}px`;

  return (
    <div
      className={cn("inline-block w-fit", className)}
      style={{
        perspective: "900px",
        // CSS custom properties for internal use
        ["--book-width" as string]: bookWidth,
        ["--book-color" as string]: color,
        ["--book-text-color" as string]: textColor || "var(--color-foreground)",
        ["--book-depth" as string]: BOOK_DEPTH,
        ["--book-border-radius" as string]: BOOK_BORDER_RADIUS,
        ["--bg-shadow" as string]: BG_SHADOW,
      }}
    >
      {/* Rotate wrapper - handles 3D rotation on hover */}
      <div
        className={cn(
          "relative w-fit",
          "[transform-style:preserve-3d]",
          "[container-type:inline-size]",
          !shouldReduceMotion && [
            "transition-transform duration-[250ms] ease-out",
            "hover:[transform:rotateY(-20deg)_scale(1.066)_translateX(-8px)]",
          ]
        )}
        style={{
          aspectRatio: "49 / 60",
          minWidth: bookWidth,
        }}
      >
        {/* Front cover */}
        <div
          className={cn(
            "absolute flex flex-col",
            "overflow-hidden",
            "[transform:translateZ(0px)]",
            "shadow-[rgba(0,0,0,0.02)_0px_1px_1px,rgba(0,0,0,0.1)_0px_4px_8px_-4px,rgba(0,0,0,0.03)_0px_16px_24px_-8px]",
            "dark:bg-[linear-gradient(rgba(255,255,255,0.1)_0%,rgba(255,255,255,0)_50%,rgba(255,255,255,0)_100%),rgb(31,31,31)]",
            "dark:shadow-[rgba(0,0,0,0.05)_0px_1.8px_3.6px,rgba(0,0,0,0.08)_0px_10.8px_21.6px,rgba(0,0,0,0.1)_0px_-0.9px_inset,rgba(255,255,255,0.1)_0px_1.8px_1.8px_inset,rgba(0,0,0,0.1)_3.6px_0px_3.6px_inset]"
          )}
          style={{
            background:
              variant === "simple" ? color : "var(--color-background)",
            borderRadius: BOOK_BORDER_RADIUS,
            height: "100%",
            width: bookWidth,
          }}
        >
          {variant === "stripe" ? (
            <StripeContent
              illustration={illustration}
              logo={logo}
              title={title}
              width={width}
            />
          ) : (
            <SimpleContent logo={logo} title={title} width={width} />
          )}

          {/* Border overlay */}
          <div
            aria-hidden
            className={cn(
              "pointer-events-none absolute inset-0",
              "border border-black/10",
              "shadow-[rgba(255,255,255,0.3)_0px_1px_2px_inset]",
              "dark:border-0"
            )}
            style={{ borderRadius: "inherit" }}
          />
        </div>

        {/* Page edges (right side) */}
        <div
          aria-hidden
          className={cn(
            "pointer-events-none absolute",
            "bg-[linear-gradient(90deg,rgb(234,234,234)_0%,rgba(0,0,0,0)_70%),linear-gradient(rgb(255,255,255),rgb(250,250,250))]",
            "dark:bg-[linear-gradient(90deg,rgb(60,60,60)_0%,rgba(0,0,0,0)_70%),linear-gradient(rgb(40,40,40),rgb(35,35,35))]"
          )}
          style={{
            height: "calc(100% - 6px)",
            top: "3px",
            transform: `translateX(calc(${bookWidth} - ${BOOK_DEPTH} / 2 - 3px)) rotateY(90deg) translateX(calc(${BOOK_DEPTH} / 2))`,
            width: `calc(${BOOK_DEPTH} - 2px)`,
          }}
        />

        {/* Back cover */}
        <div
          aria-hidden
          className="pointer-events-none absolute left-0"
          style={{
            backgroundColor:
              variant === "simple" ? color : "var(--color-background)",
            borderRadius: BOOK_BORDER_RADIUS,
            height: "100%",
            transform: `translateZ(calc(-1 * ${BOOK_DEPTH}))`,
            width: bookWidth,
          }}
        />
      </div>
    </div>
  );
};

/** Stripe variant: color band at top, white body below */
const StripeContent = ({
  title,
  illustration,
  logo,
  width,
}: {
  title: string;
  illustration?: ReactNode;
  logo?: ReactNode;
  width: number;
}) => (
  <>
    {/* Colored stripe area */}
    <div
      className="relative flex w-full flex-1 flex-row items-stretch gap-2 overflow-hidden [transform:translateZ(0px)]"
      style={{ background: "var(--book-color)" }}
    >
      {illustration ? (
        <div className="flex-1 object-cover">{illustration}</div>
      ) : (
        <div className="flex-1" />
      )}
      {/* Spine binding overlay on stripe */}
      <div
        aria-hidden
        className="absolute inset-0"
        style={{
          background: "var(--bg-shadow)",
          mixBlendMode: "overlay",
        }}
      />
    </div>

    {/* Body with title */}
    <div className="flex flex-row items-stretch">
      {/* Spine binding on body */}
      <div
        aria-hidden
        className="min-w-[8.2%] opacity-20"
        style={{ background: "var(--bg-shadow)" }}
      />
      {/* Content */}
      <div
        className="flex w-full flex-col justify-between [container-type:inline-size]"
        style={{
          gap: `${(24 / DEFAULT_WIDTH) * width}px`,
          padding: "6.1%",
        }}
      >
        <span
          className="text-balance font-semibold leading-[1.25em] tracking-[-0.02em]"
          style={{
            color: "var(--book-text-color)",
            fontSize: "10.5cqw",
          }}
        >
          {title}
        </span>
        {logo ? <div>{logo}</div> : null}
      </div>
    </div>
  </>
);

/** Simple variant: full-color cover with title */
const SimpleContent = ({
  title,
  logo,
  width,
}: {
  title: string;
  logo?: ReactNode;
  width: number;
}) => (
  <div className="flex h-full w-full flex-row items-stretch">
    {/* Spine binding */}
    <div
      aria-hidden
      className="min-w-[8.2%]"
      style={{
        background: "var(--bg-shadow)",
        mixBlendMode: "overlay",
        opacity: 1,
      }}
    />
    {/* Content */}
    <div
      className="flex w-full flex-col justify-between [container-type:inline-size]"
      style={{
        gap: `${(16 / DEFAULT_WIDTH) * width}px`,
        padding: "6.1%",
      }}
    >
      <span
        className="text-balance font-semibold leading-[1.25em] tracking-[-0.02em]"
        style={{
          color: "var(--book-text-color)",
          fontSize: "12cqw",
        }}
      >
        {title}
      </span>
      {logo ? <div>{logo}</div> : null}
    </div>
  </div>
);

export default Book;

Similar components