motionsites中文站 / Circle Buttons
Circle Buttons
aadmin2026-09-16 发布3D5 阅读

Circle Buttons

类型:3D · 分类:Buttons · 收录:2026-09-16 20:35:01 · 源码:会员可见

[hidecontent type="vip1"]
# Circle Buttons
Three compact circular icon controls using the exact Dark Glass, Launch, and Dot Border material systems.

> 来源:ThreeUI Community · https://threeui.com · MIT License(Meng To / Design+Code)
> 组件页:https://threeui.com/component/circle-buttons · 预览动画:https://threeui.com/previews/circle-buttons.webm
> 分类:Buttons
> 包含变体(3):play、plus、mail

/* ===== src/shaders/circle-buttons/CircleButtons.tsx (tsx) ===== */
import type { CSSProperties, MouseEventHandler } from "react";

import "./circle-buttons.css";

export type CircleButtonVariant = "play" | "plus" | "mail";
export type CircleButtonMode = "light" | "dark";

export type CircleButtonsProps = {
  variant?: CircleButtonVariant;
  mode?: CircleButtonMode;
  hue?: number;
  saturation?: number;
  brightness?: number;
  ariaLabel?: string;
  disabled?: boolean;
  type?: "button" | "submit" | "reset";
  onClick?: MouseEventHandler<HTMLButtonElement>;
  className?: string;
  style?: CSSProperties;
};

export const CIRCLE_BUTTON_DEFAULTS = {
  variant: "play",
  mode: "dark",
  hue: 0,
  saturation: 1,
  brightness: 1,
} as const satisfies Required<Pick<CircleButtonsProps, "variant" | "mode" | "hue" | "saturation" | "brightness">>;

const LABELS: Record<CircleButtonVariant, string> = {
  play: "Play",
  plus: "Add",
  mail: "Send mail",
};

function clamp(value: number, minimum: number, maximum: number) {
  return Math.min(maximum, Math.max(minimum, value));
}

function CircleIcon({ variant }: { variant: CircleButtonVariant }) {
  if (variant === "play") {
    return (
      <svg viewBox="0 0 32 32" aria-hidden="true">
        <path d="M11.75 8.7c0-1.28 1.4-2.08 2.5-1.43l12 7.3a1.66 1.66 0 0 1 0 2.86l-12 7.3a1.66 1.66 0 0 1-2.5-1.43V8.7Z" />
      </svg>
    );
  }

  if (variant === "plus") {
    return (
      <svg viewBox="0 0 32 32" aria-hidden="true">
        <path d="M16 7v18M7 16h18" />
      </svg>
    );
  }

  return (
    <svg viewBox="0 0 32 32" aria-hidden="true">
      <rect x="5.25" y="7.5" width="21.5" height="17" rx="3.25" />
      <path d="m7.25 10 7.35 5.72a2.26 2.26 0 0 0 2.8 0L24.75 10" />
    </svg>
  );
}

export function CircleButtons({
  variant = CIRCLE_BUTTON_DEFAULTS.variant,
  mode = CIRCLE_BUTTON_DEFAULTS.mode,
  hue = CIRCLE_BUTTON_DEFAULTS.hue,
  saturation = CIRCLE_BUTTON_DEFAULTS.saturation,
  brightness = CIRCLE_BUTTON_DEFAULTS.brightness,
  ariaLabel,
  disabled = false,
  type = "button",
  onClick,
  className,
  style,
}: CircleButtonsProps) {
  const safeVariant = variant === "plus" || variant === "mail" ? variant : "play";
  const safeMode = mode === "light" ? "light" : "dark";
  const stageStyle = {
    "--circle-button-hue": `${clamp(hue, -180, 180)}deg`,
    "--circle-button-saturation": clamp(saturation, 0, 2),
    "--circle-button-brightness": clamp(brightness, 0.35, 1.65),
    ...style,
  } as CSSProperties;

  return (
    <div
      className={`circle-buttons circle-buttons--${safeVariant} circle-buttons--${safeMode}${className ? ` ${className}` : ""}`}
      data-mode={safeMode}
      data-variant={safeVariant}
      style={stageStyle}
    >
      <div className="circle-buttons__atmosphere" aria-hidden="true" />
      <button
        className="circle-button"
        type={type}
        aria-label={ariaLabel ?? LABELS[safeVariant]}
        disabled={disabled}
        onClick={onClick}
      >
        <span className="circle-button__aura" aria-hidden="true" />
        <span className="circle-button__rim" aria-hidden="true" />
        <span className="circle-button__face" aria-hidden="true" />
        <span className="circle-button__details" aria-hidden="true">
          <i /><i /><i /><i />
          <b /><b /><b /><b />
        </span>
        <span className="circle-button__icon">
          <CircleIcon variant={safeVariant} />
        </span>
      </button>
    </div>
  );
}

/* ===== src/shaders/circle-buttons/circle-buttons.css (css) ===== */
.circle-buttons,
.circle-buttons *,
.circle-buttons *::before,
.circle-buttons *::after {
  box-sizing: border-box;
}

.circle-buttons {
  --circle-button-hue: 0deg;
  --circle-button-saturation: 1;
  --circle-button-brightness: 1;
  position: relative;
  display: grid;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  place-items: center;
  overflow: hidden;
  isolation: isolate;
  container-type: size;
  background: #111318;
  color-scheme: dark;
  filter: hue-rotate(var(--circle-button-hue)) saturate(var(--circle-button-saturation)) brightness(var(--circle-button-brightness));
}

.circle-buttons--play {
  background: #0a0a0a;
}

.circle-buttons--light {
  background: #f4f7fb;
  color-scheme: light;
}

.circle-buttons__atmosphere {
  position: absolute;
  width: min(62cqw, 440px);
  height: min(50cqh, 240px);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.02);
  filter: blur(54px);
  pointer-events: none;
}

.circle-button {
  position: relative;
  display: grid;
  width: clamp(56px, 8cqw, 72px);
  aspect-ratio: 1;
  padding: 0;
  place-items: center;
  overflow: visible;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: rgba(255, 255, 255, 0.7);
  cursor: pointer;
  transform: translateZ(0);
  transition: transform 200ms ease-in-out, filter 200ms ease-in-out;
  -webkit-tap-highlight-color: transparent;
}

.circle-button:disabled {
  cursor: not-allowed;
  filter: grayscale(0.7);
  opacity: 0.48;
}

.circle-button:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.88);
  outline-offset: 7px;
}

.circle-buttons--light .circle-button:focus-visible {
  outline-color: rgba(17, 24, 39, 0.8);
}

.circle-button__aura,
.circle-button__rim,
.circle-button__face,
.circle-button__details,
.circle-button__icon {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}

.circle-button__face {
  z-index: 1;
}

.circle-button__rim {
  z-index: 2;
}

.circle-button__details {
  z-index: 4;
}

.circle-button__icon {
  z-index: 5;
  display: grid;
  place-items: center;
}

.circle-button__icon svg {
  width: 30%;
  height: 30%;
  overflow: visible;
}

/* Play is the Dark Glass rectangle reduced to a circular boundary. */
.circle-buttons--play .circle-button {
  overflow: hidden;
}

.circle-buttons--play .circle-button__face {
  background: rgba(23, 23, 23, 0.7);
  box-shadow: inset 2px 4px 16px rgba(255, 255, 255, 0.06);
  -webkit-backdrop-filter: blur(50px);
  backdrop-filter: blur(50px);
}

.circle-buttons--play .circle-button__rim {
  border: 1px solid rgba(255, 255, 255, 0.25);
  opacity: 0.25;
  -webkit-mask-image: linear-gradient(170deg, #000, transparent 50%);
  mask-image: linear-gradient(170deg, #000, transparent 50%);
}

.circle-buttons--play .circle-button__aura {
  z-index: 3;
  top: 50%;
  left: 50%;
  width: 200px;
  height: 200px;
  background: conic-gradient(from 180deg, rgba(255, 255, 255, 0.012) 180deg, rgba(255, 255, 255, 0.05) 360deg);
  filter: blur(15px);
  transform: translate(-50%, -50%);
  animation: circle-button-section-orbit 3.6s linear infinite;
  transition: opacity 500ms;
}

.circle-buttons--play .circle-button__face::after {
  position: absolute;
  inset: -38%;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  content: "";
  opacity: 0;
  filter: blur(5px);
  transform: scale(0.5);
  transition: opacity 500ms, transform 700ms;
  -webkit-mask-image: radial-gradient(50% 50%, #000, rgba(23, 23, 23, 0));
  mask-image: radial-gradient(50% 50%, #000, rgba(23, 23, 23, 0));
}

.circle-buttons--play .circle-button__icon svg {
  margin-left: 4%;
  fill: currentColor;
}

.circle-buttons--play .circle-button:not(:disabled):hover .circle-button__aura {
  opacity: 0;
}

.circle-buttons--play .circle-button:not(:disabled):hover .circle-button__face::after {
  opacity: 1;
  transform: scale(1);
}

.circle-buttons--play .circle-button:not(:disabled):active {
  transform: scale(0.98);
}

.circle-buttons--light.circle-buttons--play .circle-button {
  color: rgba(17, 24, 39, 0.7);
}

.circle-buttons--light.circle-buttons--play .circle-button__face {
  background: rgba(232, 234, 238, 0.78);
  box-shadow: inset 2px 4px 16px rgba(255, 255, 255, 0.72);
}

.circle-buttons--light.circle-buttons--play .circle-button__rim {
  border-color: rgba(17, 24, 39, 0.28);
}

/* Plus carries the Launch rectangle's exact amber gradient, rim, depth, and press. */
.circle-buttons--plus .circle-buttons__atmosphere {
  display: none;
}

.circle-buttons--plus .circle-button {
  color: #451a03;
}

.circle-buttons--plus .circle-button__aura {
  inset: -4px;
  border-radius: inherit;
  background: rgba(245, 158, 11, 0.4);
  opacity: 0.4;
  filter: blur(8px);
  transition: opacity 500ms;
}

.circle-buttons--plus .circle-button__face {
  background: linear-gradient(to bottom, #fde68a, #fcd34d, #f59e0b);
  box-shadow:
    0 0 0 1px rgba(251, 191, 36, 0.5),
    0 4px 0 #b45309,
    0 10px 15px -3px rgba(0, 0, 0, 0.5);
  transition: transform 150ms, box-shadow 150ms;
}

.circle-buttons--plus .circle-button__icon svg {
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-width: 1.5;
}

.circle-buttons--plus .circle-button:not(:disabled):hover .circle-button__aura {
  opacity: 0.75;
}

.circle-buttons--plus .circle-button:not(:disabled):active {
  transform: translateY(2px);
}

.circle-buttons--plus .circle-button:not(:disabled):active .circle-button__face {
  box-shadow:
    0 0 0 1px rgba(251, 191, 36, 0.5),
    0 2px 0 #b45309;
}

/* Mail keeps the Dot Border rectangle's transparent face and staged technical frame. */
.circle-buttons--mail .circle-buttons__atmosphere {
  display: none;
}

.circle-buttons--mail .circle-button {
  color: rgba(255, 255, 255, 0.27);
}

.circle-buttons--mail .circle-button__face {
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: transparent;
  transition: background-color 200ms ease-in-out;
}

.circle-buttons--mail .circle-button__details {
  inset: -10px;
  border-radius: 0;
}

.circle-buttons--mail .circle-button__details i {
  position: absolute;
  width: 8px;
  aspect-ratio: 1;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.67);
  opacity: 0;
  transition: opacity 300ms ease-in-out, transform 350ms ease-in-out;
}

.circle-buttons--mail .circle-button__details i:nth-of-type(1) {
  top: -4px;
  left: -4px;
  transform: translate(34px, 34px);
}

.circle-buttons--mail .circle-button__details i:nth-of-type(2) {
  top: -4px;
  right: -4px;
  transform: translate(-34px, 34px);
  transition-delay: 210ms;
}

.circle-buttons--mail .circle-button__details i:nth-of-type(3) {
  right: -4px;
  bottom: -4px;
  transform: translate(-34px, -34px);
  transition-delay: 420ms;
}

.circle-buttons--mail .circle-button__details i:nth-of-type(4) {
  bottom: -4px;
  left: -4px;
  transform: translate(34px, -34px);
  transition-delay: 630ms;
}

.circle-buttons--mail .circle-button__details b {
  position: absolute;
  display: block;
  opacity: 0;
}

.circle-buttons--mail .circle-button__details b:nth-of-type(1),
.circle-buttons--mail .circle-button__details b:nth-of-type(3) {
  right: 0;
  left: 0;
  height: 1px;
  background-image: repeating-linear-gradient(90deg, transparent 0 2px, rgba(255, 255, 255, 0.67) 2px 4px);
  transform: scaleX(0);
}

.circle-buttons--mail .circle-button__details b:nth-of-type(1) {
  top: 0;
  transform-origin: top left;
  transition: transform 350ms ease-in-out 280ms, opacity 20ms linear 280ms;
}

.circle-buttons--mail .circle-button__details b:nth-of-type(3) {
  bottom: 0;
  transform-origin: bottom right;
  transition: transform 350ms ease-in-out 700ms, opacity 20ms linear 700ms;
}

.circle-buttons--mail .circle-button__details b:nth-of-type(2),
.circle-buttons--mail .circle-button__details b:nth-of-type(4) {
  top: 0;
  bottom: 0;
  width: 1px;
  background-image: repeating-linear-gradient(0deg, transparent 0 2px, rgba(255, 255, 255, 0.67) 2px 4px);
  transform: scaleY(0);
}

.circle-buttons--mail .circle-button__details b:nth-of-type(2) {
  right: 0;
  transform-origin: top right;
  transition: transform 350ms ease-in-out 490ms, opacity 20ms linear 490ms;
}

.circle-buttons--mail .circle-button__details b:nth-of-type(4) {
  left: 0;
  transform-origin: bottom left;
  transition: transform 350ms ease-in-out 840ms, opacity 20ms linear 840ms;
}

.circle-buttons--mail .circle-button__icon svg {
  fill: rgba(255, 255, 255, 0.13);
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 1.45;
  transition: fill 200ms ease-in-out, stroke 200ms ease-in-out;
}

.circle-buttons--mail .circle-button:not(:disabled):hover {
  color: rgba(255, 255, 255, 0.67);
  transform: scale(1.05);
}

.circle-buttons--mail .circle-button:not(:disabled):hover .circle-button__face,
.circle-buttons--mail .circle-button:not(:disabled):active .circle-button__face {
  background: #25358b;
}

.circle-buttons--mail .circle-button:not(:disabled):hover .circle-button__details i,
.circle-buttons--mail .circle-button:not(:disabled):hover .circle-button__details b {
  opacity: 1;
  transform: none;
}

.circle-buttons--mail .circle-button:not(:disabled):active {
  transform: scale(0.98);
}

.circle-buttons--light.circle-buttons--mail .circle-button {
  color: rgba(17, 24, 39, 0.38);
}

.circle-buttons--light.circle-buttons--mail .circle-button__face {
  border-color: rgba(17, 24, 39, 0.2);
}

.circle-buttons--light.circle-buttons--mail .circle-button__details i {
  background: rgba(17, 24, 39, 0.58);
}

.circle-buttons--light.circle-buttons--mail .circle-button__details b:nth-of-type(1),
.circle-buttons--light.circle-buttons--mail .circle-button__details b:nth-of-type(3) {
  background-image: repeating-linear-gradient(90deg, transparent 0 2px, rgba(17, 24, 39, 0.58) 2px 4px);
}

.circle-buttons--light.circle-buttons--mail .circle-button__details b:nth-of-type(2),
.circle-buttons--light.circle-buttons--mail .circle-button__details b:nth-of-type(4) {
  background-image: repeating-linear-gradient(0deg, transparent 0 2px, rgba(17, 24, 39, 0.58) 2px 4px);
}

.circle-buttons--light.circle-buttons--mail .circle-button:not(:disabled):hover {
  color: rgba(255, 255, 255, 0.67);
}

@keyframes circle-button-section-orbit {
  0% { transform: translate(-50%, -50%) translateX(0) rotate(0); }
  25% { transform: translate(-50%, -50%) translateX(-30px) rotate(90deg); }
  50% { transform: translate(-50%, -50%) translateX(0) rotate(180deg); }
  75% { transform: translate(-50%, -50%) translateX(30px) rotate(270deg); }
  100% { transform: translate(-50%, -50%) translateX(0) rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .circle-buttons *,
  .circle-buttons *::before,
  .circle-buttons *::after {
    scroll-behavior: auto !important;
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}
[/hidecontent]
← 返回首页