# Find files

Compose file search & filters with storage permissions, loading and failure states.

Import: @slices.sh/file-storage/headless

Public parts: 33

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/file-storage@^0.2.7
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/file-storage.md

## Anatomy

### Composition

```text
Files.Container bucket="files" ifExists="ask"
  Files.Root provided internally by Container
    Files.Filters
      Files.FilterPreset
      Files.DateRange
        Files.FilterRange
        Files.RangeInput
        Files.RangeStep
        Files.RangePreset
        Files.RangeMonths
          Files.RangeMonthName
          Files.RangeDays
            Files.RangeDay
      Files.TypeMenu
        Files.TypeGroup group="images"
          Files.TypeOptions
            Files.TypeOption
        Files.TypeSearch
        Files.TypeEmpty
      Files.WhenFiltered
        Files.AppliedFilters
          Files.WhenFilter type="fileType"
          Files.FilterValue
          Files.FilterOperator
          Files.FilterRemove
          Files.FilterType
        Files.FilterClear
    Files.Sorting
      Files.SortKey
      Files.SortActive
    Files.WhenSearching
      Files.SearchClear
    Files.Unmatched
    Files.Search
    Files.SortBy
```

## Minimal composition

```tsx
"use client"

import "@/services"
import { Files } from "@slices.sh/file-storage/headless"

export function Example() {
  return (
    <Files.Root>
      <Files.Search aria-label="Search files" />
      <Files.SearchClear>Clear search</Files.SearchClear>
      <Files.Sorting>
        <Files.SortKey by="name">Name</Files.SortKey>
        <Files.SortKey by="updatedAt">Modified</Files.SortKey>
      </Files.Sorting>
      <Files.Entries>
        <Files.Entry>
          <Files.EntryName />
          <Files.EntrySize />
        </Files.Entry>
      </Files.Entries>
      <Files.Unmatched>No matching files.</Files.Unmatched>
    </Files.Root>
  )
}

```

Configure a file-storage provider. Mutating actions follow the storage rules and current user permissions.

## Complete API reference

### Files.Filters

scope

What narrows the listing besides the words in the search box: file types and dates, as the
pills in a bar say them.

Its own part rather than something `Files.Root` always holds, because most browsers have no
filter bar and should not re-render for one. Everything that draws rows reads it where it is
provided (`useShownEntries`), so a filter chosen here is what `Files.Entries`, the keys, the
preview and select-all all agree is on screen.

Import: @slices.sh/file-storage/headless

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/filters.tsx

### Files.WhenSearching

gate

There is something in the search box: a dot on the button that opens it has a reason to be there.

Import: @slices.sh/file-storage/headless

Capability: search

Files.WhenSearching must be inside <Files.Root>.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/when-searching.tsx

### Files.Search

input · Element: input

The search box. Escape empties it, as in a toolbar search everywhere; with nothing in it
the key goes on to whatever it was pressed for, a popover that holds the box closing.

Import: @slices.sh/file-storage/headless

Capability: search

File.Search must be inside <File.Browser>.

```tsx
PartProps<"input">
```

Source: @slices.sh/file-storage/src/headless/search.tsx

### Files.SearchClear

action · Element: button

Empties the search box. Nothing while it is empty: there is nothing to clear, and a button
drawn over an empty box is a button that does nothing.

The box gets the focus back, since the person who pressed this is about to type again.

Import: @slices.sh/file-storage/headless

Capability: search

Files.SearchClear must be inside <Files.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/search-clear.tsx

### Files.WhenFiltered

gate

Something is narrowing the listing, which is when a bar of pills and a dot on the menu have a reason to be there.

Import: @slices.sh/file-storage/headless

Files.WhenFiltered must be inside <Files.Filters>.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/when-filtered.tsx

### Files.AppliedFilters

scope

One copy of its children per filter that is on, each in that filter's scope: the pills.

Import: @slices.sh/file-storage/headless

Files.AppliedFilters must be inside <Files.Filters>.

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/applied-filters.tsx

### Files.WhenFilter

gate

This pill is a filter of that kind, with that operator.

The words a pill says for what it is are the screen's, and which of them to say is a fact
about the filter: this is how a pattern chooses without reading it.

Import: @slices.sh/file-storage/headless

Files.WhenFilter must be inside <Files.AppliedFilters>.

```tsx
WhenFilterProps

type WhenFilterProps = GateProps & {
  type?: FileFilterType
  operator?: FileFilterOperator
  /** A date filter that holds a range of two days rather than one preset. */
  range?: boolean
}
```

Source: @slices.sh/file-storage/src/headless/when-filter.tsx

### Files.FilterType

content · Element: span

What kind of filter a pill is and how it compares, for the screen's words: `File type`, and
`is any of`. Without a function child it says the two in the model's own terms.

Import: @slices.sh/file-storage/headless

Files.FilterType must be inside <Files.AppliedFilters>.

```tsx
PartProps<"span", { type: FileFilterType; operator: FileFilterOperator }>
```

Source: @slices.sh/file-storage/src/headless/filter-type.tsx

### Files.FilterOperator

action · Element: button

One operator a pill may be turned into: `after` or `before` for a preset, `in range` or
`not in range` for two days, `is`, `is any of` or `is not` for types.

Only the ones this filter's value allows are drawn, so a menu of these never offers a
choice the filter would refuse. → docs/rules/capability-honesty.md

Import: @slices.sh/file-storage/headless

Files.FilterOperator must be inside <Files.AppliedFilters>.

Files.FilterOperator must be inside <Files.Filters>.

```tsx
PartProps<"button"> & { operator: FileFilterOperator }
```

Source: @slices.sh/file-storage/src/headless/filter-operator.tsx

### Files.FilterValue

content · Element: span

What a pill is filtering by: the preset or the days of a range as dates, and for a type
filter the types joined, until its function child says it in words (`3 selected`, `1 week
ago`), which are the screen's to choose.

Import: @slices.sh/file-storage/headless

Files.FilterValue must be inside <Files.AppliedFilters>.

```tsx
PartProps<"span", FilterValueState>

type FilterValueState = {
  type: FileFilterType
  operator: FileFilterOperator
  /** MIME types for a type filter; a preset, or two timestamps, for a date. */
  value: readonly string[]
  /** The two ends of a custom range, as dates; `null` for any other filter. */
  range: { from: Date; to: Date } | null
}
```

Source: @slices.sh/file-storage/src/headless/filter-value.tsx

### Files.FilterRemove

action · Element: button

Takes this pill's filter off. Its function child is told which kind it is, for a name that says what is being removed.

Import: @slices.sh/file-storage/headless

Files.FilterRemove must be inside <Files.AppliedFilters>.

Files.FilterRemove must be inside <Files.Filters>.

```tsx
PartProps<"button", { type: FileFilterType }>
```

Source: @slices.sh/file-storage/src/headless/filter-remove.tsx

### Files.FilterClear

action · Element: button

Takes every filter off. Nothing while there are none: there is nothing to clear.

Import: @slices.sh/file-storage/headless

Files.FilterClear must be inside <Files.Filters>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/filter-clear.tsx

### Files.TypeMenu

scope

A menu of file types that is searched by typing, and the words the screen has for each type
(`PDF`, `Word document`), which is what a person types against.

Mount it inside the menu, so the search starts empty each time the menu opens. The types
themselves are `Files.TypeOptions`, which follow what is typed here.

Import: @slices.sh/file-storage/headless

```tsx
RootProps & { label?: FileTypeMenu["label"] }
```

Source: @slices.sh/file-storage/src/headless/type-menu.tsx

### Files.TypeSearch

input · Element: input

The box a menu of file types is searched in.

Its keys stay in it, except Escape and Tab: a menu moves its highlight and jumps to an item
by a letter, and letters typed into a box are not a request for either.

Import: @slices.sh/file-storage/headless

Files.TypeSearch must be inside <Files.TypeMenu>.

```tsx
PartProps<"input">
```

Source: @slices.sh/file-storage/src/headless/type-search.tsx

### Files.TypeGroup

gate

A group of file types that has some: where its heading goes, so a menu never shows `Code`
over nothing.

Import: @slices.sh/file-storage/headless

```tsx
GateProps & { group: FileTypeGroup }
```

Source: @slices.sh/file-storage/src/headless/type-group.tsx

### Files.TypeOptions

scope

One copy of its children per file type among the files read, in one group when given.

Import: @slices.sh/file-storage/headless

```tsx
RootProps & { group?: FileTypeGroup }
```

Source: @slices.sh/file-storage/src/headless/type-options.tsx

### Files.TypeOption

action · Element: button

One file type as a choice that stays ticked: the first creates the type filter, and the
last one unticked takes it off.

The words (`PDF`, `Word document`) are the screen's, from the `mime` its function child is
given; a type nobody named is shown as itself.

Import: @slices.sh/file-storage/headless

Files.TypeOption must be inside <Files.TypeOptions>.

Files.TypeOption must be inside <Files.Filters>.

```tsx
PartProps<"button", TypeOptionState>

type TypeOptionState = {
  mime: string
  group: FileTypeGroup
  /** What kind of file this type is, for the icon a skin draws. */
  kind: FileKind
  checked: boolean
}
```

Source: @slices.sh/file-storage/src/headless/type-option.tsx

### Files.TypeEmpty

gate

No file type matches: what a menu of them says in place of its rows.

Import: @slices.sh/file-storage/headless

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/type-empty.tsx

### Files.Sorting

scope

The order the listing is drawn in, with the five keys a file browser sorts by: name, kind,
date created, date modified and size.

Apart from `Files.SortBy`, which asks the store for three of them one folder at a time: this
one is the screen's, over every folder it has read, and is what `Files.Entries` and
`Files.ChildEntries` follow when it is provided.

Import: @slices.sh/file-storage/headless

```tsx
RootProps & { initial?: FileOrder }
```

Source: @slices.sh/file-storage/src/headless/sorting.tsx

### Files.SortKey

action · Element: button

A key to sort by, as a column heading or a menu item.

No `aria-sort`, for the reason `Files.SortBy` has none: it means something on a
`columnheader` in a grid and nothing on a button. The direction is in `state`, so a pattern
can put it in the button's name.

Import: @slices.sh/file-storage/headless

Files.SortKey must be inside <Files.Sorting>.

```tsx
SortKeyProps

type SortKeyProps = PartProps<"button", { active: boolean; direction: "asc" | "desc" }> & {
  by: FileSortKey
  /**
   * `toggle`, for a column heading: the same key again turns the order around. `choose`, for
   * a menu item: the same key again changes nothing, and the order stays as it is.
   */
  mode?: "toggle" | "choose"
}
```

Source: @slices.sh/file-storage/src/headless/sort-key.tsx

### Files.SortActive

content · Element: span

The key the listing is in the order of, for a menu's button to say: `Modified`, with the
direction in `state` for a screen that shows an arrow. The word is the screen's; without
one it is the key.

Import: @slices.sh/file-storage/headless

Files.SortActive must be inside <Files.Sorting>.

```tsx
PartProps<"span", { key: FileSortKey; direction: FileSortDirection }>
```

Source: @slices.sh/file-storage/src/headless/sort-active.tsx

### Files.SortBy

action · Element: button

A column header that sorts. The same one twice turns the order around.

No `aria-sort`: it means something on a `columnheader` inside a grid and nothing at all on
a button, so a list of rows that is not a grid was telling assistive technology which way
it was sorted through an attribute nothing reads. What replaces it is the direction in
`state`, so the pattern can put the answer in the button's own accessible name — a word,
which is the pattern's to choose.
→ https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/

Import: @slices.sh/file-storage/headless

Files.SortBy must be inside <Files.Root>.

```tsx
SortByProps

type SortByProps = PartProps<"button", { active: boolean; direction: "asc" | "desc" }> & {
  by: FileSort
}
```

Source: @slices.sh/file-storage/src/headless/sort-by.tsx

### Files.DateRange

scope

Two days being picked for a date filter: what a custom range dialog holds while it is open.

It starts from the range the filter already has, so a pill that is pressed to change its
range opens on it, and from nothing otherwise. Nothing leaves it until `Files.FilterRange`
applies it, so closing the dialog changes nothing. Mount it inside the dialog's content: it
is then new each time the dialog opens, and starts from whatever the filter says by then.

Import: @slices.sh/file-storage/headless

Files.DateRange must be inside <Files.Filters>.

```tsx
RootProps & { date: Exclude<FileFilterType, "fileType"> }
```

Source: @slices.sh/file-storage/src/headless/range-root.tsx

### Files.FilterPreset

action · Element: button

One date preset: `1 week ago` and the rest, as a choice.

On a pill it changes that filter where it stands; in a menu, with a `date`, it starts the
date filter of that kind, `after` the preset, in place of any there was.

Import: @slices.sh/file-storage/headless

Files.FilterPreset must be inside <Files.Filters>.

```tsx
FilterPresetProps

type FilterPresetProps = PartProps<"button"> & {
  preset: DatePreset
  /** Which date to start filtering by, when this is in a menu and not on a pill. */
  date?: Exclude<FileFilterType, "fileType">
}
```

Source: @slices.sh/file-storage/src/headless/filter-preset.tsx

### Files.FilterRange

action · Element: button

Applies a custom range of days to a date filter, from the start of the first to the end of
the last.

Inside a `Files.DateRange` it applies what that has picked, and is disabled until both ends
are. Given `from` and `to` itself it applies those: what a dialog that keeps its own days
(a calendar of another library) hands over.

Import: @slices.sh/file-storage/headless

Files.FilterRange must be inside <Files.Filters>.

```tsx
FilterRangeProps

type FilterRangeProps = PartProps<"button"> & {
  /** Which date: `dateModified` or `dateCreated`. From the `Files.DateRange` it is inside, when it is inside one. */
  date?: Exclude<FileFilterType, "fileType">
  /** What the dialog has picked so far, when it is not a `Files.DateRange`. The part is disabled until both ends are. */
  from?: Date
  to?: Date
}
```

Source: @slices.sh/file-storage/src/headless/filter-range.tsx

### Files.RangePreset

action · Element: button

A shortcut for a range, `last-7-days` and the like: it fills both days, the calendar and the boxes.

Import: @slices.sh/file-storage/headless

Files.RangePreset must be inside <Files.DateRange>.

```tsx
PartProps<"button"> & { preset: RangePreset }
```

Source: @slices.sh/file-storage/src/headless/range-preset.tsx

### Files.RangeInput

input · Element: input

One end of the range as a text box, `2026-01-08`: the same two days the calendar marks.
A day typed that is not one yet stays as it was typed and moves nothing.

Import: @slices.sh/file-storage/headless

Files.RangeInput must be inside <Files.DateRange>.

```tsx
PartProps<"input"> & { end: "from" | "to" }
```

Source: @slices.sh/file-storage/src/headless/range-input.tsx

### Files.RangeStep

action · Element: button

Moves the calendar a month back or on.

Import: @slices.sh/file-storage/headless

Files.RangeStep must be inside <Files.DateRange>.

```tsx
PartProps<"button"> & { by: -1 | 1 }
```

Source: @slices.sh/file-storage/src/headless/range-step.tsx

### Files.RangeMonths

scope

The months a calendar draws, two of them side by side, each in its own scope. Whether the
second one fits is the screen's to say, with CSS: on a phone it is drawn and hidden.

Import: @slices.sh/file-storage/headless

Files.RangeMonths must be inside <Files.DateRange>.

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/range-months.tsx

### Files.RangeMonthName

content · Element: div

The month's name and year in the reader's own language, `June 2026`, unless a function child says it otherwise.

Import: @slices.sh/file-storage/headless

Files.RangeMonthName must be inside <Files.RangeMonths>.

```tsx
PartProps<"div", { month: Date; index: number }>
```

Source: @slices.sh/file-storage/src/headless/range-month-name.tsx

### Files.RangeDays

scope

Every day of the month in its scope, each with the weekday it falls on, for a grid to place.

Import: @slices.sh/file-storage/headless

Files.RangeDays must be inside <Files.RangeMonths>.

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/range-days.tsx

### Files.RangeDay

action · Element: button

One day of the calendar, a button that picks it.

`data-state` says where it is in the range: `from`, `to` (both, for a range of one day, is
`from`), `within` or `none`. The first of the month sits in its weekday's column, so the
rows of the grid line up with the names above them, `data-today` marks today, and
`data-joined` an end of a range of more than one day.

Import: @slices.sh/file-storage/headless

Files.RangeDay must be inside <Files.DateRange>.

Files.RangeDay must be inside <Files.RangeDays>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/range-day.tsx

### Files.Unmatched

gate

The folder holds entries and the filters leave none of them.

Not `Files.Empty`, which is a folder with nothing in it: the remedy is different, to take a
filter off and not to put something in. Never under a search, whose own empty answer is
`Files.Empty` with `searched`.

Import: @slices.sh/file-storage/headless

Files.Unmatched must be inside <Files.Root>.

```tsx
GateProps<{ filtered: boolean }>
```

Source: @slices.sh/file-storage/src/headless/unmatched.tsx

### Files.Container

scope

The one place a file explorer talks to the file service.

It maps the client onto what the screen below draws: the folder that is open and its pages
(`Files.Root`), the folders read besides it, from the store's listings or from
`loadChildren`, and the address of every file and every picture, which the parts ask the
client for and the server signs. What the screen may do is read here too, from what the
client has: a part that needs `upload` asks `Files.CanUpload`, so a service with none gets a
browser with no upload button and no drop, and not a button that fails when pressed.

The screen never calls the service itself: pattern code calls no hook, and every part under
here reaches the service through this container's contexts. → docs/rules/patterns-read-no-state.md

Import: @slices.sh/file-storage/headless

```tsx
ContainerProps

type ContainerProps = RootProps &
  Omit<FilesRootProps, "children"> & {
    /** Where a folder's children come from when it is not the store's own listing. */
    loadChildren?: LoadChildren
  }
```

Source: @slices.sh/file-storage/src/headless/container.tsx

### Files.Root

scope

The browser's root, and Files.Root with Files.Children in one, the seam that reads what the config's client can do.

Import: @slices.sh/file-storage/headless

```tsx
FilesRootProps

export type FilesRootProps = RootProps & {
  /** The file-storage config, a slug from the registry (`services/file-storage/<config>.config.ts`). The nearest root's, else `"default"`, when left out. */
  config?: string
  /**
   * Left out or `true`: required, and a missing service throws. `"optional"`: renders its
   * children and every part inside renders nothing except `Files.Unavailable`, and a Composer
   * inside finds its sink switched off. `false`: off. A capability name such as `"trash"`
   * requires the service with it.
   */
  enabled?: Enabled
  bucket?: string
  /** Where the browser opens. Give `path` and `onPath` instead to keep it in the address. */
  initialPath?: string
  path?: string
  onPath?(path: string): void
  multiple?: boolean
  /** Opening another folder ends a search, as in Finder, instead of carrying it into the folder. */
  searchLeavesWithFolder?: boolean
  /**
   * What moving an entry into a folder that has one of that name does. Left out, it is
   * refused. `"ask"` raises a question for `Files.Clash` to put to the person: replace, keep
   * both or skip.
   */
  ifExists?: IfExists | "ask"
}
```

Source: @slices.sh/file-storage/src/headless/files-root.tsx
