Kosmesis
Components

File Picker

A button and an input-style field that open the native file dialog via FileSelection.

Installation

npx kosmesis add file-picker
pnpm dlx kosmesis add file-picker
yarn dlx kosmesis add file-picker
bunx kosmesis add file-picker

Install the following dependencies:

npm install @praxisjs/composables @morphos/icons
pnpm add @praxisjs/composables @morphos/icons
yarn add @praxisjs/composables @morphos/icons
bun add @praxisjs/composables @morphos/icons

This component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button.

Copy and paste the following code into your project.

file-picker.tsx
import type { FileSelection } from "@praxisjs/composables";import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { Button, type ButtonProps } from "./button";import { cn } from "@/lib/utils";export interface FilePickerProps {  selection: FileSelection;  label?: string;  variant?: ButtonProps["variant"];  size?: ButtonProps["size"];  disabled?: boolean;  class?: string;}@Component()export class FilePicker extends StatelessComponent<FilePickerProps> {  render() {    const { selection, label = "Choose file", variant = "outline", size, disabled, class: cls } = this.props;    return (      <Button variant={variant} size={size} disabled={disabled} class={cls} onClick={() => { selection.open(); }}>        <Icon name="Paperclip" size={14} />        {label}      </Button>    );  }}export interface FileInputProps {  selection: FileSelection;  placeholder?: string;  buttonLabel?: string;  disabled?: boolean;  class?: string;}@Component()export class FileInput extends StatelessComponent<FileInputProps> {  render() {    const { selection, placeholder = "No file chosen", buttonLabel = "Browse", disabled, class: cls } = this.props;    return (      <button        type="button"        disabled={disabled}        class={cn(          "flex h-9 w-full min-w-0 items-center gap-3 rounded-md border border-input bg-transparent pr-3 text-left text-sm shadow-xs outline-none transition-[color,box-shadow]",          "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",          "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",          cls,        )}        onClick={() => { selection.open(); }}      >        <span class="flex h-full items-center rounded-l-md border-r border-input bg-muted px-3 font-medium">          {buttonLabel}        </span>        {() => {          const files = selection.files;          if (files.length === 0) return <span class="truncate text-muted-foreground">{placeholder}</span>;          return <span class="truncate">{files.length === 1 ? files[0].name : `${String(files.length)} files selected`}</span>;        }}      </button>    );  }}

Install the following dependencies:

npm install @praxisjs/composables @morphos/icons @praxisjs/css
pnpm add @praxisjs/composables @morphos/icons @praxisjs/css
yarn add @praxisjs/composables @morphos/icons @praxisjs/css
bun add @praxisjs/composables @morphos/icons @praxisjs/css

This component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button.

Copy and paste the following code into your project.

file-picker.tsx
import type { FileSelection } from "@praxisjs/composables";import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { Button, type ButtonProps } from "./button";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class FilePickerStyles extends Stylesheet {  $input = this.css({    display: "flex",    height: "2.25rem",    width: "100%",    minWidth: "0",    alignItems: "center",    gap: "0.75rem",    borderRadius: `calc(${t.radius} - 2px)`,    border: `1px solid ${t.input}`,    backgroundColor: "transparent",    paddingRight: "0.75rem",    textAlign: "left",    fontSize: "0.875rem",    boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)",    outline: "none",    cursor: "pointer",    transition: "color 120ms ease, box-shadow 120ms ease",  })    .focusVisible({ borderColor: t.ring, boxShadow: `0 0 0 3px color-mix(in oklab, ${t.ring} 50%, transparent)` })    .disabled({ pointerEvents: "none", cursor: "not-allowed", opacity: 0.5 });  $inputButton = this.css({    display: "flex",    height: "100%",    alignItems: "center",    borderRight: `1px solid ${t.input}`,    backgroundColor: t.muted,    padding: "0 0.75rem",    fontWeight: 500,  });  $text = this.css({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" });  $placeholder = this.css({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: t.mutedForeground });}export interface FilePickerProps {  selection: FileSelection;  label?: string;  variant?: ButtonProps["variant"];  size?: ButtonProps["size"];  disabled?: boolean;  class?: string;}@Component()export class FilePicker extends StatelessComponent<FilePickerProps> {  render() {    const { selection, label = "Choose file", variant = "outline", size, disabled, class: cls } = this.props;    return (      <Button variant={variant} size={size} disabled={disabled} class={cls} onClick={() => { selection.open(); }}>        <Icon name="Paperclip" size={14} />        {label}      </Button>    );  }}export interface FileInputProps {  selection: FileSelection;  placeholder?: string;  buttonLabel?: string;  disabled?: boolean;  class?: string;}@Component()export class FileInput extends StatelessComponent<FileInputProps> {  @Styled(FilePickerStyles) $s!: FilePickerStyles;  render() {    const { selection, placeholder = "No file chosen", buttonLabel = "Browse", disabled, class: cls } = this.props;    return (      <button type="button" disabled={disabled} class={cx(this.$s.$input, cls)} onClick={() => { selection.open(); }}>        <span class={this.$s.$inputButton}>{buttonLabel}</span>        {() => {          const files = selection.files;          if (files.length === 0) return <span class={this.$s.$placeholder}>{placeholder}</span>;          return <span class={this.$s.$text}>{files.length === 1 ? files[0].name : `${String(files.length)} files selected`}</span>;        }}      </button>    );  }}

Examples

Usage

These components are presentation for the file upload composables in @praxisjs/composables (installed automatically by the CLI). The composable is attached with @Compose in your component, so validation options (accept, maxSize, maxFiles, …) stay yours, and is passed to the Kosmesis component as a prop.

import { FileSelection } from "@praxisjs/composables";
import { FileInput, FilePicker } from "@/components/ui/file-picker";

@Component()
class Form extends StatefulComponent {
  @Compose(FileSelection, { accept: ".pdf", maxSize: 2 * 1024 * 1024 })
  selection!: FileSelection;

  render() {
    return (
      <>
        <FilePicker selection={this.selection} label="Choose file" />
        <FileInput selection={this.selection} />
      </>
    );
  }
}

FileInput looks like a text input and shows the chosen file name (or a count) in place of its placeholder. Pair either with File List to show and remove the selection.

Props

FilePicker

PropTypeDefault
selectionFileSelection— required
labelstring"Choose file"
variant / sizeButton variant / size"outline" / —
disabledboolean—
classstring—

FileInput

PropTypeDefault
selectionFileSelection— required
placeholderstring"No file chosen"
buttonLabelstring"Browse"
disabledboolean—
classstring—

On this page