Avatar Upload
A circular image picker with live preview, driven by FileSelection and FilePreview.
Installation
npx kosmesis add avatar-uploadpnpm dlx kosmesis add avatar-uploadyarn dlx kosmesis add avatar-uploadbunx kosmesis add avatar-uploadInstall the following dependencies:
npm install @praxisjs/composables @morphos/iconspnpm add @praxisjs/composables @morphos/iconsyarn add @praxisjs/composables @morphos/iconsbun add @praxisjs/composables @morphos/iconsThis 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.
import { FilePreview, type FileSelection } from "@praxisjs/composables";import { StatefulComponent } from "@praxisjs/core";import { Component, Compose, getter, Prop } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { Button } from "./button";import { cn } from "@/lib/utils";export interface AvatarUploadProps { /** Should be configured with `accept: "image/*"` and a single file (the default). */ selection: FileSelection; label?: string; class?: string;}@Component()export class AvatarUpload extends StatefulComponent { @Prop() selection!: FileSelection; @Prop() label = "Upload photo"; @Prop() class?: string; get file(): File | null { return this.selection.files[0] ?? null; } @Compose(FilePreview, getter("file")) preview!: FilePreview; render() { return ( <div class={cn("flex items-center gap-4", this.class)}> <span class="flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-full border bg-muted text-muted-foreground"> {() => this.preview.url ? ( <img src={this.preview.url} alt="" class="size-full object-cover" /> ) : ( <Icon name="User" size={24} /> ) } </span> <div class="flex flex-col items-start gap-1"> <div class="flex gap-2"> <Button variant="outline" size="sm" onClick={() => { this.selection.open(); }}> {() => (this.file ? "Change" : this.label)} </Button> {() => this.file && ( <Button variant="ghost" size="sm" onClick={() => { this.selection.clear(); }}> Remove </Button> ) } </div> {() => this.selection.errors.length > 0 && ( <p role="alert" class="text-xs text-destructive">{this.selection.errors[0].message}</p> ) } </div> </div> ); }}Install the following dependencies:
npm install @praxisjs/composables @morphos/icons @praxisjs/csspnpm add @praxisjs/composables @morphos/icons @praxisjs/cssyarn add @praxisjs/composables @morphos/icons @praxisjs/cssbun add @praxisjs/composables @morphos/icons @praxisjs/cssThis 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.
import { FilePreview, type FileSelection } from "@praxisjs/composables";import { StatefulComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, Compose, getter, Prop } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { Button } from "./button";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class AvatarUploadStyles extends Stylesheet { $root = this.css({ display: "flex", alignItems: "center", gap: "1rem" }); $preview = this.css({ display: "flex", width: "4rem", height: "4rem", flexShrink: 0, alignItems: "center", justifyContent: "center", overflow: "hidden", borderRadius: "9999px", border: `1px solid ${t.border}`, backgroundColor: t.muted, color: t.mutedForeground, }); $image = this.css({ width: "100%", height: "100%", objectFit: "cover" }); $controls = this.css({ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: "0.25rem" }); $actions = this.css({ display: "flex", gap: "0.5rem" }); $error = this.css({ margin: 0, fontSize: "0.75rem", color: t.destructive });}export interface AvatarUploadProps { /** Should be configured with `accept: "image/*"` and a single file (the default). */ selection: FileSelection; label?: string; class?: string;}@Component()export class AvatarUpload extends StatefulComponent { @Styled(AvatarUploadStyles) $s!: AvatarUploadStyles; @Prop() selection!: FileSelection; @Prop() label = "Upload photo"; @Prop() class?: string; get file(): File | null { return this.selection.files[0] ?? null; } @Compose(FilePreview, getter("file")) preview!: FilePreview; render() { return ( <div class={cx(this.$s.$root, this.class)}> <span class={this.$s.$preview}> {() => this.preview.url ? ( <img src={this.preview.url} alt="" class={this.$s.$image} /> ) : ( <Icon name="User" size={24} /> ) } </span> <div class={this.$s.$controls}> <div class={this.$s.$actions}> <Button variant="outline" size="sm" onClick={() => { this.selection.open(); }}> {() => (this.file ? "Change" : this.label)} </Button> {() => this.file && ( <Button variant="ghost" size="sm" onClick={() => { this.selection.clear(); }}> Remove </Button> ) } </div> {() => this.selection.errors.length > 0 && ( <p role="alert" class={this.$s.$error}>{this.selection.errors[0].message}</p> ) } </div> </div> ); }}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 { AvatarUpload } from "@/components/ui/avatar-upload";
@Component()
class Profile extends StatefulComponent {
@Compose(FileSelection, { accept: "image/*", maxSize: 2 * 1024 * 1024 })
selection!: FileSelection;
render() {
return <AvatarUpload selection={this.selection} />;
}
}Uses a single-file selection (the FileSelection default). The preview is a FilePreview composed inside
the component, so its object URL is cleaned up automatically.
Props
| Prop | Type | Default |
|---|---|---|
selection | FileSelection | — required |
label | string | "Upload photo" |
class | string | — |