Untitled UI React is a modern, accessible UI component library built with React, Tailwind CSS, TypeScript, and React Aria.
It provides a large set of production-ready, open-source UI components that you can copy and paste directly into your existing Next/React/Vite projects.
Features
🎨 50+ Components – Buttons, forms, navigation, data display, and specialized components.
♿ Accessibility-First – Built with React Aria for WCAG compliance and keyboard navigation.
🎯 No Dependencies – Copy and paste components directly into your project
đź”§ Full Customization – Complete source code ownership for unlimited modifications.
📱 Mobile-Responsive – Components adapt to different screen sizes and devices.
🎪 Multiple Variants – Each component includes various sizes, states, and styling options.
🏗️ CLI Installation – Quick project setup with pre-configured components.
🎨 Consistent Design System – Cohesive visual language across all components.
Use Cases
- Web Application Development: Build responsive and professional-looking web applications with a consistent design language.
- E-commerce Sites: Develop visually appealing and user-friendly online stores with a wide range of UI elements.
- SaaS Dashboard Development: Build admin panels and user dashboards with form components, data tables, and navigation elements.
- Marketing Landing Pages: Design conversion-focused pages with hero sections, testimonials, and call-to-action components.
- Mobile-First Applications: Develop responsive web apps using touch-friendly components and mobile-optimized layouts.
- Enterprise Applications: Build internal tools and business applications with complex forms, data visualization, and user management interfaces.
CLI Installation
# Next.js project
npx untitledui@latest init --nextjs
# Vite project
npx untitledui@latest init --viteManual Installation
1. Install dependencies:
npm install @untitledui/icons react-aria-components tailwindcss-react-aria-components tailwind-merge tailwindcss-animate2. Create theme.css in your project root:
@theme {
--font-body: var(--font-inter, "Inter"), -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
--font-display: var(--font-inter, "Inter"), -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
--text-xs: calc(var(--spacing) * 3);
--text-sm: calc(var(--spacing) * 3.5);
--text-md: calc(var(--spacing) * 4);
--radius-sm: 0.125rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--shadow-sm: 0px 1px 3px rgba(10, 13, 18, 0.1);
--shadow-md: 0px 4px 6px -1px rgba(10, 13, 18, 0.1);
...
}3. In your global.css:
@import "tailwindcss";
@plugin "tailwindcss-animate";
@plugin "tailwindcss-react-aria-components";
@import "./theme.css";4. Create components/utils/cx.ts:
import { extendTailwindMerge } from "tailwind-merge";
const twMerge = extendTailwindMerge({
extend: {
theme: {
text: ["display-xs", "display-sm", "display-md", "display-lg"],
},
},
});
export const cx = twMerge;Usage
1. After installation, copy components directly from the documentation into your project:
"use client";
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps, FC, ReactNode } from "react";
import React, { isValidElement } from "react";
import type { ButtonProps as AriaButtonProps } from "react-aria-components";
import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
import { cx, sortCx } from "@/utils/cx";
import { isReactComponent } from "@/utils/is-react-component";
export const styles = sortCx({
common: {
root: [
"group relative inline-flex h-max cursor-pointer items-center justify-center whitespace-nowrap outline-brand transition duration-100 ease-linear before:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
// When button is used within `InputGroup`
"in-data-input-wrapper:shadow-xs in-data-input-wrapper:focus:!z-50 in-data-input-wrapper:in-data-leading:-mr-px in-data-input-wrapper:in-data-leading:rounded-r-none in-data-input-wrapper:in-data-leading:before:rounded-r-none in-data-input-wrapper:in-data-trailing:-ml-px in-data-input-wrapper:in-data-trailing:rounded-l-none in-data-input-wrapper:in-data-trailing:before:rounded-l-none",
// Disabled styles
"disabled:cursor-not-allowed disabled:text-fg-disabled",
// Icon styles
"disabled:*:data-icon:text-fg-disabled_subtle",
// Same as `icon` but for SSR icons that cannot be passed to the client as functions.
"*:data-icon:pointer-events-none *:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:transition-inherit-all",
].join(" "),
icon: "pointer-events-none size-5 shrink-0 transition-inherit-all",
},
sizes: {
sm: {
root: [
"gap-1 rounded-lg px-3 py-2 text-sm font-semibold before:rounded-[7px] data-icon-only:p-2",
"in-data-input-wrapper:px-3.5 in-data-input-wrapper:py-2.5 in-data-input-wrapper:data-icon-only:p-2.5",
].join(" "),
linkRoot: "gap-1",
},
md: {
root: [
"gap-1 rounded-lg px-3.5 py-2.5 text-sm font-semibold before:rounded-[7px] data-icon-only:p-2.5",
"in-data-input-wrapper:gap-1.5 in-data-input-wrapper:px-4 in-data-input-wrapper:text-md in-data-input-wrapper:data-icon-only:p-3",
].join(" "),
linkRoot: "gap-1",
},
lg: {
root: "gap-1.5 rounded-lg px-4 py-2.5 text-md font-semibold before:rounded-[7px] data-icon-only:p-3",
linkRoot: "gap-1.5",
},
xl: {
root: "gap-1.5 rounded-lg px-4.5 py-3 text-md font-semibold before:rounded-[7px] data-icon-only:p-3.5",
linkRoot: "gap-1.5",
},
},
colors: {
primary: {
root: [
"bg-brand-solid text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset hover:bg-brand-solid_hover data-loading:bg-brand-solid_hover",
// Inner border gradient
"before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
// Disabled styles
"disabled:bg-disabled disabled:shadow-xs disabled:ring-disabled_subtle",
// Icon styles
"*:data-icon:text-button-primary-icon hover:*:data-icon:text-button-primary-icon_hover",
].join(" "),
},
secondary: {
root: [
"bg-primary text-secondary shadow-xs-skeumorphic ring-1 ring-primary ring-inset hover:bg-primary_hover hover:text-secondary_hover data-loading:bg-primary_hover",
// Disabled styles
"disabled:shadow-xs disabled:ring-disabled_subtle",
// Icon styles
"*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
].join(" "),
},
tertiary: {
root: [
"text-tertiary hover:bg-primary_hover hover:text-tertiary_hover data-loading:bg-primary_hover",
// Icon styles
"*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
].join(" "),
},
"link-gray": {
root: [
"justify-normal rounded-xs p-0! text-tertiary hover:text-tertiary_hover",
// Inner text underline
"*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
// Icon styles
"*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
].join(" "),
},
"link-color": {
root: [
"justify-normal rounded-xs p-0! text-brand-secondary hover:text-brand-secondary_hover",
// Inner text underline
"*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
// Icon styles
"*:data-icon:text-fg-brand-secondary_alt hover:*:data-icon:text-fg-brand-secondary_hover",
].join(" "),
},
"primary-destructive": {
root: [
"bg-error-solid text-white shadow-xs-skeumorphic ring-1 ring-transparent outline-error ring-inset",
// Inner border gradient
"before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
// Disabled styles
"disabled:bg-disabled disabled:shadow-xs disabled:ring-disabled_subtle",
// Icon styles
"*:data-icon:text-button-destructive-primary-icon hover:*:data-icon:text-button-destructive-primary-icon_hover",
].join(" "),
},
"secondary-destructive": {
root: [
"bg-primary text-error-primary shadow-xs-skeumorphic ring-1 ring-error_subtle outline-error ring-inset hover:bg-error-primary hover:text-error-primary_hover data-loading:bg-error-primary",
// Disabled styles
"disabled:bg-primary disabled:shadow-xs disabled:ring-disabled_subtle",
// Icon styles
"*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
].join(" "),
},
"tertiary-destructive": {
root: [
"text-error-primary outline-error hover:bg-error-primary hover:text-error-primary_hover data-loading:bg-error-primary",
// Icon styles
"*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
].join(" "),
},
"link-destructive": {
root: [
"justify-normal rounded-xs p-0! text-error-primary outline-error hover:text-error-primary_hover",
// Inner text underline
"*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
// Icon styles
"*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
].join(" "),
},
},
});
/**
* Common props shared between button and anchor variants
*/
export interface CommonProps {
/** Disables the button and shows a disabled state */
isDisabled?: boolean;
/** Shows a loading spinner and disables the button */
isLoading?: boolean;
/** The size variant of the button */
size?: keyof typeof styles.sizes;
/** The color variant of the button */
color?: keyof typeof styles.colors;
/** Icon component or element to show before the text */
iconLeading?: FC<{ className?: string }> | ReactNode;
/** Icon component or element to show after the text */
iconTrailing?: FC<{ className?: string }> | ReactNode;
/** Removes horizontal padding from the text content */
noTextPadding?: boolean;
/** When true, keeps the text visible during loading state */
showTextWhileLoading?: boolean;
}
/**
* Props for the button variant (non-link)
*/
export interface ButtonProps extends CommonProps, DetailedHTMLProps<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "slot">, HTMLButtonElement> {
/** Slot name for react-aria component */
slot?: AriaButtonProps["slot"];
}
/**
* Props for the link variant (anchor tag)
*/
interface LinkProps extends CommonProps, DetailedHTMLProps<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "color">, HTMLAnchorElement> {}
/** Union type of button and link props */
export type Props = ButtonProps | LinkProps;
export const Button = ({
size = "sm",
color = "primary",
children,
className,
noTextPadding,
iconLeading: IconLeading,
iconTrailing: IconTrailing,
isDisabled: disabled,
isLoading: loading,
showTextWhileLoading,
...otherProps
}: Props) => {
const href = "href" in otherProps ? otherProps.href : undefined;
const Component = href ? AriaLink : AriaButton;
const isIcon = (IconLeading || IconTrailing) && !children;
const isLinkType = ["link-gray", "link-color", "link-destructive"].includes(color);
noTextPadding = isLinkType || noTextPadding;
let props = {};
if (href) {
props = {
...otherProps,
href: disabled ? undefined : href,
// Since anchor elements do not support the `disabled` attribute and state,
// we need to specify `data-rac` and `data-disabled` in order to be able
// to use the `disabled:` selector in classes.
...(disabled ? { "data-rac": true, "data-disabled": true } : {}),
};
} else {
props = {
...otherProps,
type: otherProps.type || "button",
isPending: loading,
isDisabled: disabled,
};
}
return (
<Component
data-loading={loading ? true : undefined}
data-icon-only={isIcon ? true : undefined}
{...props}
className={cx(
styles.common.root,
styles.sizes[size].root,
styles.colors[color].root,
isLinkType && styles.sizes[size].linkRoot,
(loading || (href && (disabled || loading))) && "pointer-events-none",
// If in `loading` state, hide everything except the loading icon (and text if `showTextWhileLoading` is true).
loading && (showTextWhileLoading ? "[&>*:not([data-icon=loading]):not([data-text])]:hidden" : "[&>*:not([data-icon=loading])]:invisible"),
className,
)}
>
{/* Leading icon */}
{isValidElement(IconLeading) && IconLeading}
{isReactComponent(IconLeading) && <IconLeading data-icon="leading" className={styles.common.icon} />}
{loading && (
<svg
fill="none"
data-icon="loading"
viewBox="0 0 20 20"
className={cx(styles.common.icon, !showTextWhileLoading && "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2")}
>
{/* Background circle */}
<circle className="stroke-current opacity-30" cx="10" cy="10" r="8" fill="none" strokeWidth="2" />
{/* Spinning circle */}
<circle
className="origin-center animate-spin stroke-current"
cx="10"
cy="10"
r="8"
fill="none"
strokeWidth="2"
strokeDasharray="12.5 50"
strokeLinecap="round"
/>
</svg>
)}
{children && (
<span data-text className={cx("transition-inherit-all", !noTextPadding && "px-0.5")}>
{children}
</span>
)}
{/* Trailing icon */}
{isValidElement(IconTrailing) && IconTrailing}
{isReactComponent(IconTrailing) && <IconTrailing data-icon="trailing" className={styles.common.icon} />}
</Component>
);
};2. Or directly using CLI:
npx untitledui add buttonRelated Resources
- React Aria – https://react-spectrum.adobe.com/react-aria/ – Accessibility primitives that power Untitled UI components.
- Tailwind CSS – https://tailwindcss.com – Utility-first CSS framework used for component styling.
- shadcn/ui – https://ui.shadcn.com – Similar copy-paste component library with different design philosophy.
- Untitled UI starter kit for Next.js – https://github.com/untitleduico/untitledui-nextjs-starter-kit – An official Untitled UI starter kit for Next.js.
FAQs
Q: Is Untitled UI React free to use?
A: Yes, the open-source components of Untitled UI React are licensed under the MIT license and can be used for free in unlimited commercial projects. A PRO version with more advanced components and page examples is also available under a separate license.
Q: How is this different from installing a component library as a dependency?
A: Untitled UI React gives you the actual source code that you copy into your project. You own the code completely and can modify it without restrictions. Traditional libraries require you to work within their API constraints.
Q: Can I use only specific components without installing the entire library?
A: Yes. You can copy individual components and their dependencies directly into your project. Each component page shows exactly what files and packages are needed.






