shadimage
nostalgia-ui
uiretro-buttonsource: ok · hit

RetroButton

A Mac OS 9 styled button with raised bevel effect, active/pressed state, and disabled styling.

npx shadcn add "https://nostalgia-ui.com/r/retro-button.json"
free

Where it goes

stats

Visual style

retro

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@radix-ui/react-slot

Registry dependencies

https://nostalgia-ui.com/r/nostalgia-theme.json

Files1 · source cached 10/9/2026, 10:05:37 AM

registry/new-york/ui/retro-button.tsx
"use client"

import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

/*
 * Default-button ring. OS 9 marks the button that Return/Enter activates
 * with a thick black ring separated from the face by a platinum gap. It is
 * drawn with outer box-shadows (so it never changes the button's box) plus a
 * matching margin so it does not overlap neighbours. Class strings are
 * written out in full so Tailwind can detect them.
 */

const retroButtonVariants = cva(
  [
    "inline-flex items-center justify-center whitespace-nowrap",
    "font-[family-name:var(--os9-font-heading)] text-[12px] tracking-[0.42px] leading-[0.98]",
    "cursor-pointer select-none",
    "transition-none",
  ],
  {
    variants: {
      variant: {
        default: [
          "border border-os9-black bg-os9-gray-300 text-os9-black",
          "shadow-[inset_1px_1px_0_var(--os9-white),inset_-1px_-1px_0_var(--os9-gray-700)]",
          "active:bg-os9-gray-800 active:text-os9-white",
          "active:shadow-[inset_1px_1px_0_var(--os9-gray-700),inset_-1px_-1px_0_var(--os9-white)]",
        ],
        primary: [
          "border-[2px] border-os9-black bg-os9-gray-300 text-os9-black",
          "shadow-[inset_1px_1px_0_var(--os9-white),inset_-1px_-1px_0_var(--os9-gray-700)]",
          "active:bg-os9-gray-800 active:text-os9-white",
          "active:shadow-[inset_1px_1px_0_var(--os9-gray-700),inset_-1px_-1px_0_var(--os9-white)]",
        ],
        secondary: [
          "border-y border-x-0 border-os9-black bg-os9-gray-300 text-os9-black",
          "shadow-[inset_0_1px_0_rgba(255,255,255,0.6),inset_0_-1px_0_rgba(128,128,128,0.5)]",
          "active:bg-os9-gray-800 active:text-os9-white",
          "active:shadow-[inset_0_1px_0_rgba(38,38,38,0.4),inset_0_-1px_0_rgba(133,133,133,1)]",
        ],
      },
      size: {
        default: "h-[24px] px-4 py-0",
        sm: "h-[20px] px-2 py-0 text-[10px]",
        lg: "h-[30px] px-6 py-0 text-[12px]",
      },
      /**
       * Default (Return-key) button: adds the thick outer ring. Must come
       * after "variant" so its shadows win the tailwind-merge.
       */
      isDefault: {
        false: "focus-visible:os9-focus-ring",
        true: [
          "m-[5px]",
          "shadow-[inset_1px_1px_0_var(--os9-white),inset_-1px_-1px_0_var(--os9-gray-700),0_0_0_2px_var(--os9-gray-300),0_0_0_5px_var(--os9-black)]",
          "active:shadow-[inset_1px_1px_0_var(--os9-gray-700),inset_-1px_-1px_0_var(--os9-white),0_0_0_2px_var(--os9-gray-300),0_0_0_5px_var(--os9-black)]",
          "focus-visible:outline-none focus-visible:shadow-[inset_1px_1px_0_var(--os9-white),inset_-1px_-1px_0_var(--os9-gray-700),0_0_0_2px_var(--os9-focus),0_0_0_5px_var(--os9-black)]",
          "focus-visible:active:shadow-[inset_1px_1px_0_var(--os9-gray-700),inset_-1px_-1px_0_var(--os9-white),0_0_0_2px_var(--os9-focus),0_0_0_5px_var(--os9-black)]",
        ],
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
      isDefault: false,
    },
  }
)

type RetroButtonProps = React.ComponentProps<"button"> &
  VariantProps<typeof retroButtonVariants> & {
    asChild?: boolean
  }

const RetroButton = React.forwardRef<HTMLButtonElement, RetroButtonProps>(
  function RetroButton(
    {
      className,
      variant,
      size,
      isDefault = false,
      asChild = false,
      disabled,
      ...props
    },
    ref
  ) {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(
          retroButtonVariants({ variant, size, isDefault }),
          disabled && [
            "pointer-events-none",
            "text-os9-gray-600",
            "border-os9-gray-600",
            isDefault
              ? "shadow-[inset_1px_1px_0_var(--os9-gray-300),inset_-1px_-1px_0_var(--os9-gray-400),0_0_0_2px_var(--os9-gray-300),0_0_0_5px_var(--os9-gray-600)]"
              : "shadow-[inset_1px_1px_0_var(--os9-gray-300),inset_-1px_-1px_0_var(--os9-gray-400)]",
          ],
          className
        )}
        ref={ref}
        disabled={disabled}
        data-default={isDefault ? "" : undefined}
        {...props}
      />
    )
  }
)
RetroButton.displayName = "RetroButton"

export { RetroButton, retroButtonVariants }
export type { RetroButtonProps }

More from nostalgia-ui

Browse all components in this library →