bottega

primitives

Neu Icon Button

A 100 × 100 neumorphic square button with a floating SVG icon in the top-right and an engraved text label at the bottom. Outset token-derived shadows create a soft raised surface; hover scales the square up and deepens shadows while the icon and label float upward with color shifts; active press inverts to an inset concave shadow with a slight shrink. Token-alpha shadow mixing adapts automatically between light and dark themes with no hardcoded hex. Accessible: native button semantics, configurable aria-label (defaults to the visible label), and a focus-visible ring from --ring.

Ported from Cksunandh (MIT)


Install

1. Register the namespace (once per project):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/neu-icon-button

Usage

Usagetsx
<NeuIconButton />

Props

PropTypeDefaultDescription
iconReact.ReactNodeSVG element rendered in the top-right slot of the square. Defaults to the integration cross-mark from the original uiverse design. Should be aria-hidden (decorative); the button's accessible name comes from aria-label.
labelstringcommandVisible text label at the bottom of the square. Defaults to "command". Also used as the aria-label when no explicit aria-label is passed.

Source

neu-icon-button.tsxtsx
"use client";

import type React from "react";
import styles from "./neu-icon-button.module.css";

/** Default integration/cross SVG from the original uiverse component (MIT). */
const DEFAULT_ICON = (
  <svg
    aria-hidden="true"
    viewBox="0 0 80 80"
    width="16"
    height="16"
    fill="currentColor"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path d="M64,48L64,48h-8V32h8c8.836,0,16-7.164,16-16S72.836,0,64,0c-8.837,0-16,7.164-16,16v8H32v-8c0-8.836-7.164-16-16-16S0,7.164,0,16s7.164,16,16,16h8v16h-8l0,0l0,0C7.164,48,0,55.164,0,64s7.164,16,16,16c8.837,0,16-7.164,16-16l0,0v-8h16v7.98c0,0.008-0.001,0.014-0.001,0.02c0,8.836,7.164,16,16,16s16-7.164,16-16S72.836,48.002,64,48z M64,8c4.418,0,8,3.582,8,8s-3.582,8-8,8h-8v-8C56,11.582,59.582,8,64,8z M8,16c0-4.418,3.582-8,8-8s8,3.582,8,8v8h-8C11.582,24,8,20.417,8,16z M16,72c-4.418,0-8-3.582-8-8s3.582-8,8-8l0,0h8v8C24,68.418,20.418,72,16,72z M32,48V32h16v16H32z M64,72c-4.418,0-8-3.582-8-8l0,0v-8h7.999c4.418,0,8,3.582,8,8S68.418,72,64,72z" />
  </svg>
);

export interface NeuIconButtonProps extends React.ComponentProps<"button"> {
  /**
   * SVG element rendered in the top-right slot of the square.
   * Defaults to the integration cross-mark from the original uiverse design.
   * Should be aria-hidden (decorative); the button's accessible name comes from aria-label.
   */
  icon?: React.ReactNode;
  /**
   * Visible text label at the bottom of the square.
   * Defaults to "command". Also used as the aria-label when no explicit aria-label is passed.
   */
  label?: string;
}

export function NeuIconButton({
  icon,
  label = "command",
  className,
  "aria-label": ariaLabel,
  ...props
}: NeuIconButtonProps) {
  return (
    <button
      type="button"
      aria-label={ariaLabel ?? label}
      className={[styles.button, className].filter(Boolean).join(" ")}
      {...props}
    >
      {/* aria-hidden: the button name comes from aria-label; inner content is decorative */}
      <div className={styles.content} aria-hidden="true">
        <div className={styles.icon}>{icon ?? DEFAULT_ICON}</div>
        <p className={styles.text}>{label}</p>
      </div>
    </button>
  );
}

Dependencies

  • @bottega/tokens