Zum Inhalt springen
UI Kit
Accent

Gerät in 3D als Mockup

Ein Telefon, Tablet, Laptop oder Browserfenster in echter CSS-Perspektive, mit Kante und Rückseite. Es ruht in einem Kamerawinkel, neigt sich zum Zeiger und schwebt sanft, solange es zu sehen ist, dein Inhalt bleibt darin bedienbar.

5 VariantsEine von 6 Components in 3D-Flächen

Dein Produkt in einem echten Gerät

Ein Geräte-Mockup zeigt das Produkt dort, wo Menschen es nutzen. Dieses ist in CSS-Perspektive gebaut, mit Kante und Rückseite, der Inhalt darin bleibt echtes HTML, klickbar und lesbar. Das Gerät ruht in einem Kamerawinkel, neigt sich zum Zeiger und schwebt sanft, solange es zu sehen ist.

„Telefon neben dem Text“ dreht ein Telefon mit Metallkante zum Text. „Laptop, der sich öffnet“ klappt den Deckel beim Hereinscrollen auf, mit einem bedienbaren Dashboard darin. „Tablet auf dem Tisch“ liegt schräg mit breitem Schatten, ruhig genug für eine ganze Section. „Browserfenster im Raum“ zeigt eine Seite in Desktop-Breite als gedrehte Glasfläche. „Kamerawinkel“ ergänzt Knöpfe, die das Telefon in fünf Winkel drehen, Rückseite inklusive, als Radiogruppe mit Pfeiltasten.

Setz deine echte Oberfläche ein statt eines Screenshots, sie bleibt auf jedem Bildschirm scharf und lässt sich im Code pflegen. Nimm Laptop oder Browserfenster für Software, das Telefon für Apps und mobile Seiten. Ein Gerät pro Section hält den Blick beim Produkt.

Variants

Telefon neben dem TextNeu in 1.2

Ein Telefon, zum Text gedreht, mit sichtbarer Metallkante. Es folgt der Maus um wenige Grad und schwebt, solange es zu sehen ist.

Laptop, der sich öffnetNeu in 1.2

Ein Laptop mit Tastatur, von oben gesehen. Der Deckel öffnet sich am Scharnier, sobald der Laptop ins Bild scrollt, das Dashboard darin ist echt.

Tablet auf dem TischNeu in 1.2

Ein Tablet liegt schräg auf dem Tisch, mit sichtbarer Kante und breitem Schatten. Ruhig genug, um eine ganze Sektion daneben zu tragen.

Browserfenster im RaumNeu in 1.2

Ein Browserfenster mit schmaler Kante, nach links gedreht wie eine Glasscheibe. Die Seite darin rendert in Desktopbreite und bleibt klickbar.

KamerawinkelNeu in 1.2

Knöpfe unter dem Telefon drehen es in fünf Winkel, die Rückseite eingeschlossen. Eine Radiogruppe, die Pfeiltasten wechseln den Winkel.

Details

Was Gerät in 3D mitbringt, aus Registry und Quellcode gelesen.

Kategorie
3D-Flächen
Bereich
Visuals
Variants
5
Version
1.0.0 · seit 1.2.0
Datei
components/ui/device-3d.tsx
Bringt mit
utilskitmockupdevice-framesdepth
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 8 eigene Texte
Studio
Im Code anpassen
In Sammlungen
3D und Bewegung

Props

Die Schnittstelle Device3DProps aus components/ui/device-3d.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • deviceoptional
    Device3DKind

    phonetabletlaptopbrowser

  • angleoptional
    Device3DAngle

    frontleftrightherotopdeskback

    The camera angle the device rests at

  • widthoptional
    number | string

    Width of the device body in px (or any CSS width). It never grows past its container.

  • viewportoptional
    number

    Render the content at this CSS width and scale it to fit, like a screenshot

  • imageoptional
    string

    An image instead of children, shown from the top

  • imageAltoptional
    string
  • urloptional
    string

    browser: address in the bar

  • interactiveoptional
    boolean

    Lean toward a mouse or pen. Default true.

  • floatoptional
    boolean

    Float up and down gently while on screen. Default true.

  • anglesoptional
    Device3DAngle[]

    Camera buttons under the device, one per angle. Arrow keys move between them.

  • lidoptional
    'view' | 'open'

    laptop: the lid opens when the laptop scrolls into view (default) or stands open

  • shadowoptional
    boolean

    Soft shadow on the floor. Default true.

  • labeloptional
    string

    Accessible name of the device, e.g. "Northwind app on a phone"

  • captionoptional
    string

    A short visible caption under the device

  • childrenoptional
    React.ReactNode

Eingebaute Texte

Diese Texte schreibt Gerät in 3D selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 8 Texte anzeigen
anglesEnglisch Camera angleDeutsch Kamerawinkel
frontEnglisch FrontDeutsch Vorn
leftEnglisch LeftDeutsch Links
rightEnglisch RightDeutsch Rechts
heroEnglisch TurnedDeutsch Gedreht
topEnglisch AboveDeutsch Von oben
deskEnglisch On the deskDeutsch Auf dem Tisch
backEnglisch BackDeutsch Rückseite

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Gerät in 3D reagiert selbst auf die Tasten Pfeil hoch, Pfeil runter, Pfeil links, Pfeil rechts, Pos1, Ende und Leertaste.
  • Setzt aria-checked und aria-label, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rollen label, radio und radiogroup.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 325 Zeilen
components/ui/device-3d.tsx
'use client';

import { useEffect, useRef, useState } from 'react';
import { cn, prefersReducedMotion, round, useTexts } from '@/lib/kit';
import { BrowserMockup, MockupScreen, PhoneMockup } from '@/components/ui/mockup';
import { TabletDevice } from '@/components/ui/device-frames';
import { Glare, Slab, ease3d, useActive, usePointerTilt } from '@/components/ui/depth';

/*
 * A device in real CSS perspective: a phone, a tablet, a laptop or a browser window with thickness,
 * turned to a camera angle, leaning toward a mouse and floating gently while it is on screen. The
 * content comes as children (or an image) and keeps working inside, links and buttons included.
 *
 * Every device is a body with an edge (Slab): copies of its outline stacked behind the face, so a
 * turn shows a side instead of a paper sheet, and the back shows a back. The laptop is built from
 * two bodies, a base lying flat and a lid on a hinge that opens when the laptop scrolls into view.
 *
 * Without JavaScript, on touch screens and with reduced motion the device rests at its angle: a
 * finished still picture. Pointer lean and floating run only while the device is on screen.
 *
 *   <Device3D device="phone" angle="right"><App /></Device3D>
 *   <Device3D device="laptop" angle="hero" viewport={1280}>…</Device3D>
 *   <Device3D device="phone" angles={['front', 'left', 'right', 'back']}>…</Device3D>
 */

export type Device3DKind = 'phone' | 'tablet' | 'laptop' | 'browser';
export type Device3DAngle = 'front' | 'left' | 'right' | 'hero' | 'top' | 'desk' | 'back';

export interface Device3DProps {
  device?: Device3DKind;
  /** The camera angle the device rests at */
  angle?: Device3DAngle;
  /** Width of the device body in px (or any CSS width). It never grows past its container. */
  width?: number | string;
  /** Render the content at this CSS width and scale it to fit, like a screenshot */
  viewport?: number;
  /** An image instead of children, shown from the top */
  image?: string;
  imageAlt?: string;
  /** browser: address in the bar */
  url?: string;
  /** Lean toward a mouse or pen. Default true. */
  interactive?: boolean;
  /** Float up and down gently while on screen. Default true. */
  float?: boolean;
  /** Camera buttons under the device, one per angle. Arrow keys move between them. */
  angles?: Device3DAngle[];
  /** laptop: the lid opens when the laptop scrolls into view (default) or stands open */
  lid?: 'view' | 'open';
  /** Soft shadow on the floor. Default true. */
  shadow?: boolean;
  /** Accessible name of the device, e.g. "Northwind app on a phone" */
  label?: string;
  /** A short visible caption under the device */
  caption?: string;
  labels?: Partial<(typeof TEXTS)['en']>;
  className?: string;
  children?: React.ReactNode;
}

const TEXTS = {
  en: { angles: 'Camera angle', front: 'Front', left: 'Left', right: 'Right', hero: 'Turned', top: 'Above', desk: 'On the desk', back: 'Back' },
  de: { angles: 'Kamerawinkel', front: 'Vorn', left: 'Links', right: 'Rechts', hero: 'Gedreht', top: 'Von oben', desk: 'Auf dem Tisch', back: 'Rückseite' },
};

type Turn = { x: number; y: number; z: number };

/** Rotations per device. A standing device tips back to show its face from above; the laptop tips toward the camera to show its deck. */
const ANGLES: Record<'flat' | 'laptop', Record<Device3DAngle, Turn>> = {
  flat: {
    front: { x: 0, y: 0, z: 0 },
    left: { x: 4, y: 26, z: 0 },
    right: { x: 4, y: -26, z: 0 },
    hero: { x: 8, y: -22, z: 3 },
    top: { x: 34, y: 0, z: 0 },
    desk: { x: 48, y: 0, z: -30 },
    back: { x: 4, y: 180, z: 0 },
  },
  laptop: {
    front: { x: -10, y: 0, z: 0 },
    left: { x: -12, y: 30, z: 0 },
    right: { x: -12, y: -30, z: 0 },
    hero: { x: -14, y: -22, z: 0 },
    top: { x: -36, y: 0, z: 0 },
    desk: { x: -30, y: -38, z: 0 },
    back: { x: -12, y: 180, z: 0 },
  },
};

const WIDTH: Record<Device3DKind, number> = { phone: 280, tablet: 560, laptop: 720, browser: 820 };
const cq = (n: number) => `${round(n, 3)}cqw`;
const METAL = 'color-mix(in oklab, var(--foreground) 34%, var(--background))';
const INK = 'var(--bezel)';

/* ------------------------------------------------------------------ */
/* Bodies                                                              */
/* ------------------------------------------------------------------ */

function PhoneBody({ viewport, image, imageAlt, children }: Pick<Device3DProps, 'viewport' | 'image' | 'imageAlt' | 'children'>) {
  // PhoneMockup draws the face; its body is 97 % of the width, aspect 71 / 147, corner 15 %
  const back = (
    <span className="absolute inset-0" style={{ background: `linear-gradient(160deg, color-mix(in oklab, ${METAL} 82%, white), ${METAL} 55%, color-mix(in oklab, ${METAL} 88%, black))` }}>
      <span className="absolute grid grid-cols-2 gap-[14%] p-[14%] shadow-[0_2px_10px_rgb(0_0_0/0.25)]" style={{ left: cq(6), top: cq(5), width: cq(34), height: cq(34), borderRadius: cq(9), background: `color-mix(in oklab, ${METAL} 70%, black)` }}>
        {[0, 1, 2].map((i) => (
          <span key={i} className="rounded-full" style={{ background: 'radial-gradient(circle at 38% 36%, color-mix(in oklab, var(--primary) 50%, #1a1d24), #0d0f14 62%)', boxShadow: '0 0 0 2px color-mix(in oklab, #fff 18%, transparent)' }} />
        ))}
      </span>
    </span>
  );
  return (
    <Slab depth={cq(7)} layers={7} radius={cq(15)} inset={`0 ${cq(1.5)}`} edge={METAL} back={back}>
      <PhoneMockup width="100%" viewport={viewport} image={image} imageAlt={imageAlt}>
        {children}
      </PhoneMockup>
      <Glare className="inset-x-[1.5%] rounded-[15cqw]" />
    </Slab>
  );
}

function TabletBody({ viewport, image, imageAlt, children }: Pick<Device3DProps, 'viewport' | 'image' | 'imageAlt' | 'children'>) {
  const radius = cq(7.4 * (1775 / 2497) * 0.985);
  const back = (
    <span className="absolute inset-0" style={{ background: `linear-gradient(160deg, color-mix(in oklab, ${METAL} 82%, white), ${METAL} 60%)` }}>
      <span className="absolute rounded-full" style={{ left: cq(4), top: cq(4), width: cq(5), height: cq(5), background: `color-mix(in oklab, ${METAL} 60%, black)` }} />
    </span>
  );
  return (
    <Slab depth={cq(2.4)} layers={5} radius={radius} inset={`0 ${cq(0.75)}`} edge={METAL} back={back}>
      <TabletDevice width="100%" orientation="landscape" viewport={viewport}>
        {image ? <img src={image} alt={imageAlt ?? ''} className="size-full object-cover object-top" /> : children}
      </TabletDevice>
      <Glare className="inset-x-[0.75%] rounded-[5.2cqw]" />
    </Slab>
  );
}

function BrowserBody({ url, viewport, image, imageAlt, children }: Pick<Device3DProps, 'url' | 'viewport' | 'image' | 'imageAlt' | 'children'>) {
  return (
    <Slab depth={cq(1.6)} layers={4} radius="12px" edge="color-mix(in oklab, var(--foreground) 16%, var(--background))">
      <BrowserMockup url={url} aspect="16 / 10" viewport={viewport} image={image} imageAlt={imageAlt}>
        {children}
      </BrowserMockup>
      <Glare strength={0.2} className="rounded-xl" />
    </Slab>
  );
}

/** Lid on a hinge over a base lying flat. The lid angle comes from --lid (degrees, 0 is upright). */
function LaptopBody({ viewport, image, imageAlt, open, children }: Pick<Device3DProps, 'viewport' | 'image' | 'imageAlt' | 'children'> & { open: boolean }) {
  const lidH = 64;
  const deck = 60;
  return (
    <div className="relative [transform-style:preserve-3d]" style={{ width: '100%', height: cq(lidH) }}>
      {/* base, hinged at the bottom of the lid, lying toward the camera */}
      <div className="absolute inset-x-0 [transform-style:preserve-3d]" style={{ top: cq(lidH), height: cq(deck), transformOrigin: 'top', transform: 'rotateX(90deg)' }}>
        <Slab depth={cq(1.8)} layers={5} radius={`${cq(1.2)} ${cq(1.2)} ${cq(3)} ${cq(3)}`} edge={METAL} className="size-full">
          <div aria-hidden className="relative" style={{ height: cq(deck), borderRadius: `${cq(1.2)} ${cq(1.2)} ${cq(3)} ${cq(3)}`, background: `linear-gradient(180deg, color-mix(in oklab, ${METAL} 70%, white), ${METAL})` }}>
            {/* keyboard well with keys drawn as a pattern, no hundred elements */}
            <span
              className="absolute isolate"
              style={{
                left: cq(9),
                right: cq(9),
                top: cq(5),
                height: cq(26),
                borderRadius: cq(1),
                background: INK,
                backgroundImage: `repeating-linear-gradient(90deg, transparent 0 ${cq(0.45)}, color-mix(in oklab, ${INK} 86%, white) ${cq(0.45)} ${cq(5.4)}), repeating-linear-gradient(180deg, transparent 0 ${cq(0.45)}, transparent ${cq(0.45)} ${cq(5.3)})`,
                maskImage: `repeating-linear-gradient(180deg, black 0 ${cq(4.6)}, transparent ${cq(4.6)} ${cq(5.3)})`,
              }}
            />
            {/* trackpad */}
            <span className="absolute isolate left-1/2 -translate-x-1/2" style={{ top: cq(35), width: cq(28), height: cq(19), borderRadius: cq(1.6), background: `color-mix(in oklab, ${METAL} 88%, white)`, boxShadow: `inset 0 0 0 ${cq(0.15)} color-mix(in oklab, ${METAL} 70%, black)` }} />
          </div>
        </Slab>
      </div>
      {/* lid */}
      <div
        className="absolute inset-0 [transform-style:preserve-3d] motion-safe:transition-transform"
        style={{ transformOrigin: 'bottom', transform: `rotateX(var(--lid-angle, ${open ? 8 : -88}deg))`, transitionTimingFunction: ease3d, transitionDuration: 'var(--lid-duration, 1400ms)' }}
      >
        <Slab depth={cq(1)} layers={4} radius={`${cq(2.2)} ${cq(2.2)} ${cq(0.6)} ${cq(0.6)}`} edge={METAL} back={<span className="absolute inset-0" style={{ background: `linear-gradient(160deg, color-mix(in oklab, ${METAL} 80%, white), ${METAL})` }} />} className="size-full">
          <div className="relative size-full" style={{ height: cq(lidH), padding: `${cq(2.4)} ${cq(2.2)} ${cq(3.2)}`, borderRadius: `${cq(2.2)} ${cq(2.2)} ${cq(0.6)} ${cq(0.6)}`, background: INK, boxShadow: `inset 0 0 0 ${cq(0.2)} ${METAL}` }}>
            <span aria-hidden className="absolute left-1/2 -translate-x-1/2 rounded-full bg-[color-mix(in_oklab,var(--bezel)_60%,var(--foreground))]" style={{ top: cq(0.9), width: cq(0.6), height: cq(0.6) }} />
            <div data-clip-ok className="relative size-full overflow-hidden bg-background text-foreground" style={{ borderRadius: cq(0.5) }}>
              <MockupScreen viewport={viewport} guess={680} image={image} imageAlt={imageAlt}>
                {children}
              </MockupScreen>
            </div>
            <Glare strength={0.22} />
          </div>
        </Slab>
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Camera buttons, a radio group                                       */
/* ------------------------------------------------------------------ */

function AngleSwitch({ angles, value, onChange, t }: { angles: Device3DAngle[]; value: Device3DAngle; onChange: (a: Device3DAngle) => void; t: (typeof TEXTS)['en'] }) {
  const refs = useRef<(HTMLButtonElement | null)[]>([]);
  const key = (e: React.KeyboardEvent, i: number) => {
    const n = angles.length;
    const to = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? (i + 1) % n : e.key === 'ArrowLeft' || e.key === 'ArrowUp' ? (i - 1 + n) % n : e.key === 'Home' ? 0 : e.key === 'End' ? n - 1 : -1;
    if (to < 0) return;
    e.preventDefault();
    onChange(angles[to]);
    refs.current[to]?.focus();
  };
  return (
    <div role="radiogroup" aria-label={t.angles} className="relative z-10 mx-auto mt-10 kit-no-scrollbar flex w-fit max-w-full gap-1 sm:justify-center overflow-x-auto rounded-xl max-sm:w-full border border-border bg-card p-1 shadow-[var(--highlight),var(--shadow-sm)]">
      {angles.map((a, i) => {
        const on = a === value;
        return (
          <button
            key={a}
            ref={(n) => {
              refs.current[i] = n;
            }}
            type="button"
            role="radio"
            aria-checked={on}
            tabIndex={on ? 0 : -1}
            onClick={() => onChange(a)}
            onKeyDown={(e) => key(e, i)}
            className={cn(
              'min-h-11 cursor-pointer whitespace-nowrap rounded-lg px-3.5 text-[13.5px] max-sm:flex-auto max-sm:px-1.5 max-sm:text-[13px] font-medium transition-colors duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-primary sm:min-h-9 sm:px-4',
              on ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground',
            )}
          >
            {t[a]}
          </button>
        );
      })}
    </div>
  );
}

/* ------------------------------------------------------------------ */

/** A device in real perspective with thickness, leaning toward the pointer. */
export function Device3D({ device = 'phone', angle = 'hero', width, viewport, image, imageAlt, url, interactive = true, float = true, angles, lid = 'view', shadow = true, label, caption, labels, className, children }: Device3DProps) {
  const t = useTexts(TEXTS, labels);
  const [current, setCurrent] = useState<Device3DAngle>(angle);
  useEffect(() => setCurrent(angle), [angle]);
  const tilt = usePointerTilt<HTMLDivElement>();
  const [watch, active] = useActive<HTMLDivElement>();
  // The lid stands open on the server and without JavaScript; it closes only once the script knows it can open it again in view
  const [open, setOpen] = useState(true);
  useEffect(() => {
    if (device !== 'laptop' || lid === 'open' || prefersReducedMotion()) return;
    const el = watch.current;
    if (!el || el.getBoundingClientRect().top < window.innerHeight * 0.55) return;
    setOpen(false);
    const io = new IntersectionObserver(([e]) => {
      if (e.isIntersecting) {
        setOpen(true);
        io.disconnect();
      }
    }, { rootMargin: '0px 0px -22% 0px' });
    io.observe(el);
    return () => io.disconnect();
  }, [device, lid, watch]);

  const set = ANGLES[device === 'laptop' ? 'laptop' : 'flat'][current];
  const w = width ?? WIDTH[device];
  const lean = interactive ? { x: device === 'laptop' ? 8 : 12, y: device === 'laptop' ? 5 : 8 } : { x: 0, y: 0 };
  // Room for what the turn projects outside the layout box: the laptop deck, a device lying on a desk
  const room = device === 'laptop' ? (current === 'top' ? 34 : current === 'desk' ? 30 : 24) : 0;

  return (
    <div className={cn('relative w-full', device === 'laptop' && 'px-[7%]', className)}>
      {/* The laptop deck comes toward the camera and projects wider than the lid: the side room keeps it inside narrow containers */}
      <div ref={watch} className="relative mx-auto w-full [container-type:inline-size]" style={{ maxWidth: w }} role={label ? 'group' : undefined} aria-label={label}>
        <div
          ref={tilt.ref}
          {...(interactive ? tilt.handlers : {})}
          className="relative [perspective:340cqw]"
          style={{ paddingBottom: cq(room), perspectiveOrigin: '50% 40%' }}
        >
          {shadow && (
            <span
              aria-hidden
              className="pointer-events-none absolute left-1/2 -translate-x-1/2 rounded-[50%] bg-[color-mix(in_oklab,var(--foreground)_20%,transparent)] blur-2xl transition-[width,opacity] duration-700 dark:bg-[color-mix(in_oklab,black_70%,transparent)]"
              style={{ bottom: device === 'laptop' ? cq(-2) : cq(-6), width: current === 'desk' || current === 'top' ? '96%' : '78%', height: cq(device === 'phone' ? 14 : 8) }}
            />
          )}
          <div className={cn('[transform-style:preserve-3d]', float && 'motion-safe:animate-kit-float', float && !active && '[animation-play-state:paused]')}>
            <div className="[transform-style:preserve-3d]" style={{ transform: `rotateX(calc(var(--tilt-y, 0) * ${-lean.y}deg)) rotateY(calc(var(--tilt-x, 0) * ${lean.x}deg))` }}>
              <div
                className="[transform-style:preserve-3d] motion-safe:transition-transform motion-safe:duration-[1100ms]"
                style={{ transform: `rotateX(${set.x}deg) rotateY(${set.y}deg) rotateZ(${set.z}deg)`, transitionTimingFunction: ease3d }}
              >
                {device === 'phone' && <PhoneBody viewport={viewport} image={image} imageAlt={imageAlt}>{children}</PhoneBody>}
                {device === 'tablet' && <TabletBody viewport={viewport} image={image} imageAlt={imageAlt}>{children}</TabletBody>}
                {device === 'browser' && <BrowserBody url={url} viewport={viewport} image={image} imageAlt={imageAlt}>{children}</BrowserBody>}
                {device === 'laptop' && <LaptopBody viewport={viewport} image={image} imageAlt={imageAlt} open={open}>{children}</LaptopBody>}
              </div>
            </div>
          </div>
        </div>
      </div>
      {angles && angles.length > 1 && <AngleSwitch angles={angles} value={current} onChange={setCurrent} t={t} />}
      {caption && <p className="relative mt-8 text-center text-[13.5px] text-muted-foreground">{caption}</p>}
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* The body alone, for scenes that turn it themselves                  */
/* ------------------------------------------------------------------ */

/**
 * Only the device body with its edge and back, without camera, lean or float. Put it inside your
 * own 3D stage (transform-style: preserve-3d) and turn it there, e.g. in rotate-showcase.
 * It sizes itself to its container width, which must be a size container ([container-type:inline-size]).
 */
export function DeviceBody({ device = 'phone', viewport, image, imageAlt, url, open = true, children }: Pick<Device3DProps, 'device' | 'viewport' | 'image' | 'imageAlt' | 'url' | 'children'> & { open?: boolean }) {
  if (device === 'tablet') return <TabletBody viewport={viewport} image={image} imageAlt={imageAlt}>{children}</TabletBody>;
  if (device === 'browser') return <BrowserBody url={url} viewport={viewport} image={image} imageAlt={imageAlt}>{children}</BrowserBody>;
  if (device === 'laptop') return <LaptopBody viewport={viewport} image={image} imageAlt={imageAlt} open={open}>{children}</LaptopBody>;
  return <PhoneBody viewport={viewport} image={image} imageAlt={imageAlt}>{children}</PhoneBody>;
}

Häufige Fragen

Wie installiere ich Gerät in 3D?

Mit aiw add @ai-website/device-3d. Der Befehl legt die Datei components/ui/device-3d.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind mockup, device-frames und depth. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Gerät in 3D?

5 Varianten, Telefon neben dem Text, Laptop, der sich öffnet, Tablet auf dem Tisch, Browserfenster im Raum und Kamerawinkel.

Kann ich die Texte von Gerät in 3D übersetzen?

Ja. Gerät in 3D bringt 8 eigene Texte auf Deutsch und Englisch mit, etwa „Kamerawinkel“, „Vorn“ und „Links“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Gerät in 3D einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Gerät in 3D mit aiw_components und installieren sie mit aiw_add @ai-website/device-3d.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert