Theming

Theming

Components anatomy

UploadButton

Simplified component structure:

<div className={className} data-state={}>
  <label data-ut-element="button" data-state={/* "ready" | "readying" | "uploading" */}>
    <input />
    /button content goes here/
  </label>
  <div data-ut-element="allowed-content" data-state={/* "ready" | "readying" | "uploading" */}>
    /allowed content text goes here/
  </div>
</div>

UploadButton consists of three themeable elements: container, button, and allowed content.

⚠️

Note: UploadButton's button element is defined using label

Upload button anatomy

UploadDropzone

Simplified component structure:

<div className={className} data-state={/* "ready" | "readying" | "uploading" */}>
  <svg data-ut-element="upload-icon" data-state={/* "ready" | "readying" | "uploading" */}>
    ...
  </svg>
  <label data-ut-element="label" data-state={/* "ready" | "readying" | "uploading" */}>
    {/* label content goes here */}
    <input disabled={!ready} />
  </label>
  <div data-ut-element="allowed-content" data-state={/* "ready" | "readying" | "uploading" */}>
    /allowed content goes here/
  </div>
  <button
    data-ut-element="button"
    data-state={/* "ready" | "readying" | "uploading" */}
    disabled={isUploading}
  >
    {/* button content goes here */}
  </button>
</div>

UploadDropzone consists of five themeable elements: container, upload icon, label, button, and allowed content.

⚠️

Note: While in UploadButton the button element is defined using label, in UploadDropzone it is defined using button. As an abstraction layer, the button element in these two components has a special data attribute applied: data-ut-element="button".

Upload dropzone anatomy

Theming props

className

Both UploadButton and UploadDropzone accept a className prop. It allows you to pass any additional classes to the component. All classes that are being passed through this prop are going to be applied to the outermost element - container.

appearance

Both UploadButton and UploadDropzone accept an appearance prop. It accepts an object with keys that correspond to elements of a component. The interfaces for the appearance prop for UploadButton and UploadDropzone are:

type StyleField =
  | string
  | CSSProperties
  | ((args: CallbackArguments) => string | CSSProperties);
 
type UploadButtonProps = {
  /* rest of props */
  appearance?: {
    container?: StyleField;
    button?: StyleField;
    allowedContent?: StyleField;
  };
};
 
type UploadDropzoneProps = {
  /* rest of props */
  appearance?: {
    container?: StyleField;
    uploadIcon?: StyleField;
    label?: StyleField;
    allowedContent?: StyleField;
    button?: StyleField;
  };
};

where the CallbackArguments is defined as (depending on the component):

For Signal-based frameworks, such as Solid.js (opens in a new tab), the attributes of the interfaces are getter-methods. For example, the ButtonCallbackArguments.ready: boolean property is a getter-method ButtonCallbackArguments.ready(): boolean in Solid.js.

type ButtonCallbackArguments = {
  ready: boolean;
  isUploading: boolean;
  uploadProgress: number;
  fileTypes: string[];
};
 
type DropzoneCallbackArguments = {
  ready: boolean;
  isUploading: boolean;
  uploadProgress: number;
  fileTypes: string[];
  isDragActive: boolean;
};

How to theme

With TailwindCSS

Configuring TailwindCSS

To leverage the best developer experience, we strongly recommend wrapping your Tailwind config with our utility function withUt. This utility function adds additional classes and variants used to style our components.

In addition, it also automatically sets the content option to include all the necessary classes that the components use. This allows you to avoid having duplicated styles in your bundle. Therefore, when using withUt, you should not import our stylesheet into your app. If you choose not to use withUt, you have to import the default stylesheet to make the components look right.

tailwind.config.ts
import { withUt } from "uploadthing/tw";
 
export default withUt({
  // your config goes here
});
tailwind.config.js
// @ts-check
const { withUt } = require("uploadthing/tw");
 
module.exports = withUt({
  // your config goes here
});

If you're not wrapping your config as shown above, you have to import our stylesheet into your app. Otherwise, components will not look right.

app/layout.tsx
import "@uploadthing/react/styles.css";
 
// ...

Style using the className prop

className accepts any classes and will merge them using tailwind-merge, meaning you can pass any class you like and it will be applied correctly, overriding the default styles if necessary.

The withUt wrapper adds custom variants that you can leverage to easily target different elements of the component and its state:

VariantDescription
ut-button:Applied to the button element.
ut-allowed-content:Applied to the allowed content element.
ut-label:Applied to the label element.
ut-upload-icon:Applied to the upload icon element.
------
ut-readying:Applied to the container element when the component is readying.
ut-ready:Applied to the container element when the component is ready.
ut-uploading:Applied to the container element when the component is uploading.
💡

If you're not using the withUt wrapper, the state variants can be applied using data-[state="..."]:

These variants and classes can be used in conjunction with each other to make component match your design in the exact way you want.

<UploadButton
    className="mt-4 ut-button:bg-red-500 ut-button:ut-readying:bg-red-500/50"
               |    └─ applied to the button └─ applied to the button when readying
               └─ applied to the container
/>
Allowed content
Allowed content
<UploadDropzone
    className="bg-slate-800 ut-label:text-lg ut-allowed-content:ut-uploading:text-red-300"
               |            |                └─ applied to the allowed content when uploading
               |            └─ applied to the label
               └─ applied to the container
 
/>

Style using the appearance prop

If you're not using the withUt wrapper, or prefer splitting your styles up a bit, you can use the appearance prop to target the different elements of the component.

<UploadButton
  appearance={{
    button:
      "ut-ready:bg-green-500 ut-uploading:cursor-not-allowed rounded-r-none bg-red-500 bg-none after:bg-orange-400",
    container: "w-max flex-row rounded-md border-cyan-300 bg-slate-800",
    allowedContent:
      "flex h-8 flex-col items-center justify-center px-2 text-white",
  }}
/>
Allowed content
Allowed content

With custom classes

className prop

className prop accepts any classes so you can pass there anything you like. When it comes to custom classes, you can use data attributes to target specific elements of components.

<UploadButton className="custom-class" />
/* applied to container */
.custom-class {
  background-color: none;
}
 
/* applied to button */
.custom-class > *[data-ut-element="button"] {
  font-size: 1.6rem;
  color: rgb(0 0 0 / 1);
  background-color: rgb(239 68 68 / 1);
}
 
/* applied to button when uploading */
.custom-class > *[data-ut-element="button"][data-state="readying"] {
  background-color: rgb(239 68 68 / 0.5);
  color: rgb(0 0 0 / 0.5);
  cursor: not-allowed;
}
 
/* applied to the button when uploading */
.custom-class > *[data-ut-element="button"][data-state="uploading"] {
  background-color: rgb(239 68 68 / 0.5);
  color: rgb(0 0 0 / 0.5);
  cursor: not-allowed;
}
 
/* applied to the upload indicator when uploading */
.custom-class > *[data-ut-element="button"][data-state="uploading"]::after {
  background-color: rgb(234 88 12 / 1);
}
Allowed content
Allowed content
Allowed content

appearance prop

If you need, you can pass classes directly to specific elements of components or provide a callback that will be called with the current state of the component and will return a string

<UploadButton
  appearance={{
    button({ ready, isUploading }) {
      return `custom-button ${
        ready ? "custom-button-ready" : "custom-button-not-ready"
      } ${isUploading ? "custom-button-uploading" : ""}`;
    },
    container: "custom-container",
    allowedContent: "custom-allowed-content",
  }}
/>
/* applied to container */
.custom-container {
  background-color: none;
  margin-top: 1rem;
}
 
/* applied to container when readying */
.custom-container[data-state="readying"] {
  background-color: none;
}
 
/* applied to button */
.custom-button {
  font-size: 1.6rem;
  color: rgb(0 0 0 / 1);
  background-color: rgb(239 68 68 / 1);
}
 
/* applied to button when uploading */
.custom-button-uploading {
  background-color: rgb(239 68 68 / 0.5);
  color: rgb(0 0 0 / 0.5);
  cursor: not-allowed;
}
 
.custom-button-uploading::after {
  background-color: rgb(234 88 12 / 1) !important;
}
 
/* applied to the button when ready */
.custom-button-ready {
  color: #ecfdf5;
}
 
/* applied to the button when not ready */
.custom-button-not-ready {
  background-color: rgb(239 68 68 / 0.5);
  color: rgb(0 0 0 / 0.5);
  cursor: not-allowed;
}
Allowed content
Allowed content
Allowed content

With inline styles

appearance prop

If you need, you can pass inline styles directly to specific elements of component or provide a callback that will be called with the current state of the component and will return a CSSProperties object

<UploadButton
  appearance={{
    button({ ready, isUploading }) {
      return {
        fontSize: "1.6rem",
        color: "black",
        ...(ready && { color: "#ecfdf5" }),
        ...(isUploading && { color: "#d1d5db" }),
      };
    },
    container: {
      marginTop: "1rem",
    },
    allowedContent: {
      color: "#a1a1aa",
    },
  }}
/>
Allowed content
Allowed content
Allowed content

Content customisation

To customise the content of UploadButton and UploadDropzone, you can use the content prop that accepts an object with the following shape:

ReactNode in the type definitions below will be the equivalent depending on the framework you use, e.g. JSX.Element in Solid.js.

type ContentField = ReactNode | ((args: CallbackArguments) => ReactNode);
 
type UploadButtonProps = {
  /* rest of props */
  content?: {
    button?: ContentField;
    allowedContent?: ContentField;
  };
};
 
type UploadDropzoneProps = {
  /* rest of props */
  content?: {
    uploadIcon?: ContentField;
    label?: ContentField;
    allowedContent?: ContentField;
    button?: ContentField;
  };
};

When you take over the content of an element, you get full responsibility to control the different states of the component. For example, if you customize the button element, we will not show a spinner when the component is uploading.

If you're using svelte, checkout the svelte docs.

Example

<UploadButton
  content={{
    button({ ready }) {
      if (ready) return <div>Upload stuff</div>;
 
      return "Getting ready...";
    },
    allowedContent({ ready, fileTypes, isUploading }) {
      if (!ready) return "Checking what you allow";
      if (isUploading) return "Seems like stuff is uploading";
      return `Stuff you can upload: ${fileTypes.join(", ")}`;
    },
  }}
/>
Checking what you allow
Stuff you can upload:
Seems like stuff is uploading