Domain-neutral property controls for React applications built on COSS, Base UI, and Tailwind CSS.
The package focuses on compact, composable primitives for common property patterns such as select, person, multi-person, and date. It gives you the interaction model, accessible trigger composition, and formatting hooks. Your application keeps ownership of business meaning, permissions, validation, persistence, and remote state.
SelectProperty: single-choice property control with typed optionsPersonProperty: person reference control with avatar, name, and optional descriptionMultiPersonProperty: multi-select person control with compact selected-value summaryDateProperty: formatted date control with calendar popover and clear actioncore: lightweight domain types without React or visual dependencies
The public surface is intentionally neutral. The package does not define product-specific controls such as StatusProperty, PriorityProperty, OwnerProperty, or AssigneeProperty.
- Neutral by default: consumer code decides the business label, iconography, and copy
- Button-based trigger surface: the interactive trigger is a
Button, not aBadge - Controlled-first API: value, callbacks, and selection state stay explicit
- COSS-first distribution: registry dependencies are declared so the package can be copied into a consuming app
- No universal renderer: each primitive is intentionally specific to its interaction model
- React 19
- Base UI 1.x
- Tailwind CSS 4
- COSS registry primitives available in the consuming project
Install the published package:
pnpm add @tc96/properties
# or
bun add @tc96/properties
# or
npm install @tc96/propertiesInstall the peer dependencies in the consumer:
pnpm add react react-dom @base-ui/react tailwindcssThis package is distributed both as npm code and as a registry item. The registry route copies the source into the consuming app and points to @components/patterns/properties.
Add the COSS aliases in components.json:
{
"$schema": "https://ui.shadcn.com/schema.json",
"tsx": true,
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"patterns": "@/components/patterns"
}
}Then install the registry item:
bun add @tc96/properties
bunx shadcn@latest add ./node_modules/@tc96/properties/registry/properties.jsonThe registry item declares these primitives as dependencies:
@coss/avatar@coss/button@coss/calendar@coss/popover@coss/select
Tailwind should scan the installed package:
@import "tailwindcss";
@source "../node_modules/@tc96/properties/dist";import { SelectProperty, type SelectPropertyOption } from "@tc96/properties";
type Category = "alpha" | "beta";
const categories: SelectPropertyOption<Category>[] = [
{ id: "alpha", label: "Alpha" },
{ id: "beta", label: "Beta" },
];
export function CategoryProperty({
value,
onChange,
}: {
value: Category;
onChange: (value: Category) => void;
}) {
return (
<SelectProperty
ariaLabel="Category"
options={categories}
value={value}
onValueChange={onChange}
/>
);
}import { PersonProperty } from "@tc96/properties";
const people = [
{
value: "alex",
name: "Alex Rivera",
description: "alex@example.com",
avatar: { src: alexAvatarUrl, fallback: "AR" },
},
];
<PersonProperty
ariaLabel="Reviewer"
options={people}
placeholder="Select a person"
value={reviewerId}
onValueChange={setReviewerId}
/>name is the primary label, description is optional secondary text, and avatar may provide an image source plus fallback initials.
<MultiPersonProperty
ariaLabel="Assignees"
options={people}
placeholder="Add assignees"
value={assigneeIds}
onValueChange={setAssigneeIds}
/><DateProperty
ariaLabel="Target date"
fallback="No date"
locale="en-US"
timeZone="UTC"
value={targetDate}
onValueChange={setTargetDate}
/>Date formatting, serialization, calendar placement, and clear behavior are configurable.
| Package owns | Consumer owns |
|---|---|
| accessible trigger composition | business meaning of each property |
| generic option, person, and date presentation | authorization and available options |
| controlled values and callbacks | remote state, validation, and mutations |
| date parsing, formatting, and serialization hooks | storage format and time policies |
| read-only presentation | workflows, navigation, and side effects |
@tc96/properties/core exports lightweight definitions without React or visual dependencies:
import type { PropertyDefinition } from "@tc96/properties/core";
const category: PropertyDefinition<"select"> = {
id: "category",
label: "Category",
type: "select",
};Use the core contract when you need domain types without rendering primitives.
Use Bun 1.3.14 and Node 24.18.0.
bun install
bun run lint:ci
bun run typecheck
bun test
bun run buildMIT © Gabriel Melo.