All components
File Upload
Dashed dropzone with a file item list showing names, sizes, and delete triggers.
Drag files here or click to browse
- brand-guidelines.pdf1.26 MB
- logo.svg18.4 kB
1234567891011121314151617181920212223242526272829303132import { FileUpload } from "#components/ui/ark-ui/file-upload.tsx";
export function Example() { return ( // machine parts styled by the recipe — ItemDeleteTrigger ships a // default close icon; ItemName/ItemSizeText fill in from the file <FileUpload.Root maxFiles={5}> <FileUpload.Label>Attachments</FileUpload.Label> <FileUpload.Dropzone> <FileUpload.DropzoneContent> Drag files here or click to browse </FileUpload.DropzoneContent> </FileUpload.Dropzone> <FileUpload.ItemGroup> <FileUpload.Context> {(api) => api.acceptedFiles.map((file) => ( <FileUpload.Item key={file.name} file={file}> <FileUpload.ItemContent> <FileUpload.ItemName /> <FileUpload.ItemSizeText /> </FileUpload.ItemContent> <FileUpload.ItemDeleteTrigger /> </FileUpload.Item> )) } </FileUpload.Context> </FileUpload.ItemGroup> <FileUpload.HiddenInput /> </FileUpload.Root> );}Install
1npx stylexblocks add file-uploadSource
components/ui/ark-ui/file-upload.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232import * as stylex from "@stylexjs/stylex";import { FileUpload as ArkFileUpload } from "@ark-ui/react/file-upload";import { createSlotRecipeContext } from "#lib/ark-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles, type StyleOverrideProps } from "#lib/recipe-props.ts";import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const fileUploadRoot = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._4, width: '100%', alignItems: 'flex-start', },})
const fileUploadLabel = stylex.create({ base: { fontWeight: fontWeights.medium, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, },})
const fileUploadDropzone = stylex.create({ base: { backgroundColor: semanticColors.bg, borderRadius: semanticRadii.panel, borderWidth: '1px', borderStyle: 'dashed', display: 'flex', alignItems: 'center', flexDirection: 'column', gap: spacing._4, justifyContent: 'center', padding: spacing._4, transition: 'backgrounds', outlineWidth: { default: null, ':focus-visible': '1px', }, outlineOffset: { default: null, ':focus-visible': '2px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, },})
const fileUploadDropzoneContent = stylex.create({ base: { display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center', gap: spacing._4, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, },})
const fileUploadItem = stylex.create({ base: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, animationName: 'fade-in', animationDuration: durations.moderate, backgroundColor: semanticColors.bg, borderRadius: semanticRadii.panel, borderWidth: '1px', width: '100%', display: 'flex', alignItems: 'center', gap: spacing._3, paddingInline: spacing._4, paddingBlock: spacing._2, },})
const fileUploadItemGroup = stylex.create({ base: { width: '100%', display: 'flex', flexDirection: 'column', gap: spacing._3, },})
const fileUploadItemName = stylex.create({ base: { color: semanticColors.fg, fontWeight: fontWeights.medium, lineClamp: 1, },})
const fileUploadItemContent = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._0_5, flex: 1, },})
const fileUploadItemSizeText = stylex.create({ base: { color: semanticColors.fgMuted, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, },})
const fileUploadItemDeleteTrigger = stylex.create({ base: { alignSelf: 'flex-start', },})
const fileUploadItemPreviewImage = stylex.create({ base: { width: sizes._10, height: sizes._10, objectFit: 'scale-down', },})
const fileUploadRecipe = defineSlotRecipe({ slots: { root: { styles: fileUploadRoot }, label: { styles: fileUploadLabel }, dropzone: { styles: fileUploadDropzone }, dropzoneContent: { styles: fileUploadDropzoneContent }, item: { styles: fileUploadItem }, itemGroup: { styles: fileUploadItemGroup }, itemName: { styles: fileUploadItemName }, itemContent: { styles: fileUploadItemContent }, itemSizeText: { styles: fileUploadItemSizeText }, itemDeleteTrigger: { styles: fileUploadItemDeleteTrigger }, itemPreviewImage: { styles: fileUploadItemPreviewImage }, }, defaultVariants: {},});
const chrome = stylex.create({ btnReset: { appearance: "none", backgroundColor: "transparent", borderWidth: 0, borderStyle: "none", padding: 0, margin: 0, fontFamily: "inherit", fontSize: "inherit", color: "inherit", cursor: "pointer", textAlign: "inherit", }, listReset: { listStyle: "none", margin: 0, padding: 0 },});
function CloseIcon(props: React.ComponentProps<"svg">) { return ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}> <path d="m4 4 8 8m0-8-8 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> </svg> );}
const { withProvider, withContext } = createSlotRecipeContext(fileUploadRecipe);
const StyledItemDeleteTrigger = withContext( ArkFileUpload.ItemDeleteTrigger, (v) => [chrome.btnReset, ...resolveSlotStyles(fileUploadRecipe.slots.itemDeleteTrigger, v)], "FileUploadItemDeleteTrigger",);
function ItemDeleteTrigger( props: React.ComponentProps<typeof ArkFileUpload.ItemDeleteTrigger> & StyleOverrideProps,) { const { children, ...rest } = props; return ( <StyledItemDeleteTrigger {...rest}>{children ?? <CloseIcon />}</StyledItemDeleteTrigger> );}ItemDeleteTrigger.displayName = "FileUploadItemDeleteTrigger";
/** * FileUpload — Ark UI's file-upload machine parts styled with the StyleX * slot recipe: a dashed dropzone plus a file item list with names, sizes, * and delete triggers (which ship a default close icon). */export const FileUpload = { Root: withProvider(ArkFileUpload.Root, "root", "FileUploadRoot"), Label: withContext(ArkFileUpload.Label, "label", "FileUploadLabel"), Dropzone: withContext(ArkFileUpload.Dropzone, "dropzone", "FileUploadDropzone"), DropzoneContent: withContext("div", "dropzoneContent", "FileUploadDropzoneContent"), Trigger: withContext(ArkFileUpload.Trigger, () => [chrome.btnReset], "FileUploadTrigger"), ItemGroup: withContext( ArkFileUpload.ItemGroup, (v) => [...resolveSlotStyles(fileUploadRecipe.slots.itemGroup, v), chrome.listReset], "FileUploadItemGroup", ), Item: withContext(ArkFileUpload.Item, "item", "FileUploadItem"), ItemContent: withContext("div", "itemContent", "FileUploadItemContent"), ItemName: withContext(ArkFileUpload.ItemName, "itemName", "FileUploadItemName"), ItemSizeText: withContext(ArkFileUpload.ItemSizeText, "itemSizeText", "FileUploadItemSizeText"), ItemPreviewImage: withContext( ArkFileUpload.ItemPreviewImage, "itemPreviewImage", "FileUploadItemPreviewImage", ), ItemDeleteTrigger, HiddenInput: ArkFileUpload.HiddenInput, Context: ArkFileUpload.Context,};