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

PropTypeDescription
idstringUnique element ID for label association
namestringForm field name for submission
valueTCurrent field value (bindable)
requiredbooleanWhether the field is required
readOnlybooleanWhether the field is disabled
errorsstring[]Validation error messages
configRecord<string, unknown>Additional field configuration
Previous
Styling Guide