Accessible Copy-paste UI Components for shadcn – Dice UI

Copy-paste accessible UI components built on shadcn/ui. Includes data tables, media players, and 30+ TypeScript components with Tailwind CSS.

Dice UI is a UI component library that extends shadcn/ui with fully accessible, production-ready, more complex components like data tables, media players, drag-and-drop interfaces, and more.

Each component adheres to WCAG guidelines, includes appropriate ARIA attributes, and supports keyboard navigation. You add components to your project the same way you add shadcn/ui components.

Features

🏗️ Built on Radix UI: Uses Radix UI primitives as the foundation for reliable, tested component behavior.

🔧 Advanced Components: Includes complex UI patterns like sortable lists, kanban boards, data tables, and media players that are tedious to build from scratch.

⚛️ TypeScript Support: Written in TypeScript with full type definitions for better development experience and fewer runtime errors.

🎨 Full Customization: Built entirely with Tailwind CSS utility classes.

Use Cases

  • Interactive Dashboards: Use the Data Table component to display and manage large datasets with sorting and filtering capabilities.
  • Project Management Tools: Implement the Kanban component to create drag-and-drop task boards for tracking project progress.
  • Media-Rich Websites: Integrate the Media Player for a consistent and accessible video or audio playback experience.
  • Creative Web Applications: Apply the Angle Slider for unique and intuitive rotational input controls in design tools or interactive displays.
  • Form-Heavy Applications: Implement advanced form controls like color pickers, angle sliders, and mask inputs that provide better user experience than standard HTML inputs.

How to Use It

1. Install components directly from the Dice UI registry using the shadcn CLI. The angle slider component serves as an example:

npx shadcn@latest add "https://diceui.com/r/angle-slider"

This command downloads the component code and its dependencies, placing them in your project’s component directory. You can replace angle-slider with any component name from the library.

2. When you need more control over the installation, follow the manual setup steps.

2.1 Install the required peer dependency:

npm install @radix-ui/react-slot

2.2 Add the ref composition utility to your project. Create a file at lib/compose-refs.ts and add the utility functions that handle multiple refs on a single component. This utility comes from Radix UI and handles callback refs and RefObject types correctly.

/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx
*/

import * as React from "react";

type PossibleRef<T> = React.Ref<T> | undefined;

/**
* Set a given ref to a given value
* This utility takes care of different types of refs: callback refs and RefObject(s)
*/
function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === "function") {
return ref(value);
}

if (ref !== null && ref !== undefined) {
ref.current = value;
}
}

/**
* A utility to compose multiple refs together
* Accepts callback refs and RefObject(s)
*/
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
return (node) => {
let hasCleanup = false;
const cleanups = refs.map((ref) => {
const cleanup = setRef(ref, node);
if (!hasCleanup && typeof cleanup === "function") {
hasCleanup = true;
}
return cleanup;
});

// React <19 will log an error to the console if a callback ref returns a
// value. We don't use ref cleanups internally so this will only happen if a
// user's ref callback returns a value, which we only expect if they are
// using the cleanup functionality added in React 19.
if (hasCleanup) {
return () => {
for (let i = 0; i < cleanups.length; i++) {
const cleanup = cleanups[i];
if (typeof cleanup === "function") {
cleanup();
} else {
setRef(refs[i], null);
}
}
};
}
};
}

/**
* A custom hook that composes multiple refs
* Accepts callback refs and RefObject(s)
*/
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values
return React.useCallback(composeRefs(...refs), refs);
}

export { composeRefs, useComposedRefs };

2.3 Create the visually hidden input component at components/visually-hidden-input.tsx. This component provides hidden inputs that maintain form compatibility while keeping the UI clean. It syncs with visible controls and dispatches proper input events for form libraries to detect changes.

"use client";

import * as React from "react";

type InputValue = string[] | string;

interface VisuallyHiddenInputProps<T = InputValue>
extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"value" | "checked" | "onReset"
> {
value?: T;
checked?: boolean;
control: HTMLElement | null;
bubbles?: boolean;
}

function VisuallyHiddenInput<T = InputValue>(
props: VisuallyHiddenInputProps<T>,
) {
const {
control,
value,
checked,
bubbles = true,
type = "hidden",
style,
...inputProps
} = props;

const isCheckInput = React.useMemo(
() => type === "checkbox" || type === "radio" || type === "switch",
[type],
);
const inputRef = React.useRef<HTMLInputElement>(null);

const prevValueRef = React.useRef<{
value: T | boolean | undefined;
previous: T | boolean | undefined;
}>({
value: isCheckInput ? checked : value,
previous: isCheckInput ? checked : value,
});

const prevValue = React.useMemo(() => {
const currentValue = isCheckInput ? checked : value;
if (prevValueRef.current.value !== currentValue) {
prevValueRef.current.previous = prevValueRef.current.value;
prevValueRef.current.value = currentValue;
}
return prevValueRef.current.previous;
}, [isCheckInput, value, checked]);

const [controlSize, setControlSize] = React.useState<{
width?: number;
height?: number;
}>({});

React.useLayoutEffect(() => {
if (!control) {
setControlSize({});
return;
}

setControlSize({
width: control.offsetWidth,
height: control.offsetHeight,
});

if (typeof window === "undefined") return;

const resizeObserver = new ResizeObserver((entries) => {
if (!Array.isArray(entries) || !entries.length) return;

const entry = entries[0];
if (!entry) return;

let width: number;
let height: number;

if ("borderBoxSize" in entry) {
const borderSizeEntry = entry.borderBoxSize;
const borderSize = Array.isArray(borderSizeEntry)
? borderSizeEntry[0]
: borderSizeEntry;
width = borderSize.inlineSize;
height = borderSize.blockSize;
} else {
width = control.offsetWidth;
height = control.offsetHeight;
}

setControlSize({ width, height });
});

resizeObserver.observe(control, { box: "border-box" });
return () => {
resizeObserver.disconnect();
};
}, [control]);

React.useEffect(() => {
const input = inputRef.current;
if (!input) return;

const inputProto = window.HTMLInputElement.prototype;
const propertyKey = isCheckInput ? "checked" : "value";
const eventType = isCheckInput ? "click" : "input";
const currentValue = isCheckInput ? checked : value;

const serializedCurrentValue = isCheckInput
? checked
: typeof value === "object" && value !== null
? JSON.stringify(value)
: value;

const descriptor = Object.getOwnPropertyDescriptor(inputProto, propertyKey);

const setter = descriptor?.set;

if (prevValue !== currentValue && setter) {
const event = new Event(eventType, { bubbles });
setter.call(input, serializedCurrentValue);
input.dispatchEvent(event);
}
}, [prevValue, value, checked, bubbles, isCheckInput]);

const composedStyle = React.useMemo<React.CSSProperties>(() => {
return {
...style,
...(controlSize.width !== undefined && controlSize.height !== undefined
? controlSize
: {}),
border: 0,
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: "1px",
margin: "-1px",
overflow: "hidden",
padding: 0,
position: "absolute",
whiteSpace: "nowrap",
width: "1px",
};
}, [style, controlSize]);

return (
<input
type={type}
{...inputProps}
ref={inputRef}
aria-hidden={isCheckInput}
tabIndex={-1}
defaultChecked={isCheckInput ? checked : undefined}
style={composedStyle}
/>
);
}

export { VisuallyHiddenInput };

2.3 Copy the component code from the Dice UI documentation for your chosen component and place it in your components directory.

3. Import the component parts and compose them together. Here’s how the angle slider works:

import {
  AngleSlider,
  AngleSliderRange,
  AngleSliderTrack,
  AngleSliderThumb,
  AngleSliderValue,
} from "@/components/ui/angle-slider";
export default function AngleControl() {
  return (
    <AngleSlider defaultValue={45} onValueChange={(value) => console.log(value)}>
      <AngleSliderTrack>
        <AngleSliderRange />
      </AngleSliderTrack>
      <AngleSliderThumb />
      <AngleSliderValue />
    </AngleSlider>
  );
}

4. Available Components:

Form controls include Checkbox Group, Color Picker, Editable, File Upload, Input Group, Mask Input, Tags Input, Angle Slider, and Color Swatch.

Data display components cover Circular Progress, Data Table, Kbd, Listbox, QR Code, Rating, and Relative Time Card.

Layout and organization tools provide Kanban, Masonry, Scroller, Stack, and Stepper.

Interactive components include Combobox, Cropper, Marquee, Media Player, Mention, and Sortable.

Utilities offer Client Only, Composition, Direction Provider, Hitbox, Portal, Presence, Visually Hidden Input, and Visually Hidden.

Related Resources

  • shadcn/ui: The foundational component library that Dice UI extends, providing the core design system and CLI tooling.
  • Radix UI: The unstyled, accessible component primitives that power both shadcn/ui and Dice UI.
  • Tailwind CSS Documentation: Complete reference for the utility classes used to style Dice UI components.

FAQs

Q: Can I use Dice UI without shadcn/ui already installed?
A: Yes, you can use Dice UI independently. The CLI will install the necessary dependencies when you add components. You don’t need to have shadcn/ui components already in your project.

Q: How do I update components after installing them?
A: Since components are copied into your project, you have full control over the code. Check the Dice UI documentation for updates and manually copy new versions. This approach gives you complete ownership and prevents breaking changes from affecting your project.

Q: Does Dice UI replace shadcn/ui?
A: No, Dice UI is an extension of shadcn/ui. It provides additional, more complex components that follow the same design and implementation principles.

Q: Can I customize the appearance of Dice UI components?
A: Yes, the components are built with Tailwind CSS and are fully customizable. You can modify them to match your project’s design system just like any other shadcn/ui component.

Sadman Sakib

Sadman Sakib

Leave a Reply

Your email address will not be published. Required fields are marked *