Customization
Custom Components
Overview
Vextro renders field inputs from collection metadata. Replace built-in renderers with custom Svelte components for field inputs (edit view) and list cells (list view).
Creating a Custom Input
Custom inputs receive VextroFieldProps and serialize their value via a hidden input.
Define the Convex validator first, then derive the TypeScript type with Infer from convex/values. This keeps the validator as the single source of truth — no manual type duplication.
// addressValidator.ts
import { v } from "convex/values";
import type { Infer } from "convex/values";
export const addressValidator = v.object({ street: v.string(), city: v.string(), zip: v.string() });
export type Address = Infer<typeof addressValidator>; <!-- AddressField.svelte -->
<script lang="ts">
import type { VextroFieldProps } from "vextro";
import type { Address } from "./addressValidator";
let { name, value = $bindable<Address>(), readOnly }: VextroFieldProps<Address> = $props();
let street = $state(value?.street ?? "");
let city = $state(value?.city ?? "");
let zip = $state(value?.zip ?? "");
</script>
<div class="vextro-field-group">
<input class="vextro-input" bind:value={street} disabled={readOnly} placeholder="Street" />
<input class="vextro-input" bind:value={city} disabled={readOnly} placeholder="City" />
<input class="vextro-input" bind:value={zip} disabled={readOnly} placeholder="ZIP" />
<input type="hidden" {name} value={JSON.stringify({ street, city, zip })} />
</div> Registering and Using
Register the type, then reference it with f.custom() in a collection definition. Pass the same validator to f.custom() — this ensures the Convex schema and your component type stay in sync.
import { registerFieldType } from "vextro/fields";
import { f, defineVextroCollection } from "vextro";
import { addressValidator } from "./addressValidator";
registerFieldType({ type: "address", label: "Address", description: "Multi-part address" });
export const stores = defineVextroCollection({
slug: "stores", label: "Stores", collectionType: "content", tableName: "stores",
fields: { name: f.text({ required: true }), address: f.custom(addressValidator, "address") },
}); Providing Components via Config
Pass overrides through inputOverrides in VextroConfig. Keys match field type names.
const config: VextroConfig = {
brandName: "My Admin",
inputOverrides: { components: { address: AddressField }, context: { apiKey: "..." } },
}; Props Reference
| Prop | Type | Description |
|---|---|---|
id | string | Unique element ID for label association |
name | string | Form field name for submission |
value | T | Current field value (bindable) |
required | boolean | Whether the field is required |
readOnly | boolean | Whether the field is disabled |
errors | string[] | Validation error messages |
config | Record<string, unknown> | Additional field configuration |