Framework Integration
Refine components are native Web Components — they are HTML elements. You set attributes, listen to DOM events, and project content through slots. Every framework that renders HTML works the same way, with minor wiring differences per framework.
Before you start
Complete installation and import the global styles first. This guide covers framework-specific wiring only.
React
React 19
React 19 has first-class support for web components. Custom events bind with camelCase props, boolean attributes work without [attr.*], and properties set correctly without a ref.
import '@vielzeug/refine/button';
import '@vielzeug/refine/input';
function ContactForm() {
const [email, setEmail] = useState('');
return (
<form>
<ore-input
label="Email"
type="email"
value={email}
onChange={(e: CustomEvent<{ value: string }>) => setEmail(e.detail.value)}
/>
<ore-button variant="solid" color="primary" type="submit">
Send
</ore-button>
</form>
);
}React 18 and Earlier
React 18 does not forward custom events through JSX props. Use a ref and addEventListener for any event that carries event.detail.
import { useEffect, useRef } from 'react';
import '@vielzeug/refine/input';
function SearchBox() {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const handler = (e: Event) => {
const value = (e as CustomEvent<{ value: string }>).detail.value;
console.log('Search:', value);
};
el.addEventListener('change', handler);
return () => el.removeEventListener('change', handler);
}, []);
return <ore-input ref={ref} label="Search" />;
}Native browser events (click, focus, blur) work through JSX props as normal in both React versions.
TypeScript
Create a global declaration file so TypeScript recognises ore-* tags in JSX:
// src/custom-elements.d.ts
declare namespace React {
namespace JSX {
interface IntrinsicElements {
'ore-button': React.HTMLAttributes<HTMLElement> & {
variant?: 'solid' | 'outline' | 'ghost';
color?: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
};
'ore-input': React.HTMLAttributes<HTMLElement> & {
label?: string;
type?: string;
value?: string;
required?: boolean;
disabled?: boolean;
};
// extend for each component you use
}
}
}Vite Setup
Tell the React plugin to treat ore-* tags as custom elements so it doesn't warn about unknown JSX elements:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
include: /\.(jsx|tsx)$/,
}),
],
});Vue 3
Vue 3 supports web components natively. Custom events bind with @event-name, attributes bind with :attr, and no extra configuration is needed for basic usage.
<template>
<ore-input
label="Email"
type="email"
:value="email"
:disabled="isLoading"
@change="email = ($event as CustomEvent<{ value: string }>).detail.value"
/>
<ore-button variant="solid" color="primary" :loading="isLoading" @click="submit">
Submit
</ore-button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import '@vielzeug/refine/button';
import '@vielzeug/refine/input';
const email = ref('');
const isLoading = ref(false);
function submit() {
isLoading.value = true;
}
</script>Vite Setup
Tell the Vue compiler to treat ore-* as custom elements so it skips component resolution for them:
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('ore-'),
},
},
}),
],
});Two-Way Binding
v-model does not work on web components. Bind :value and @change explicitly:
<ore-input
:value="email"
@change="email = ($event as CustomEvent<{ value: string }>).detail.value"
label="Email"
/>Svelte
Svelte handles web components natively — no configuration needed.
<script>
import '@vielzeug/refine/button';
import '@vielzeug/refine/input';
let email = $state('');
</script>
<ore-input
label="Email"
value={email}
onchange={(e) => (email = e.detail.value)}
/>
<ore-button variant="solid" color="primary" onclick={() => console.log(email)}>
Submit
</ore-button>Svelte 4
In Svelte 4, use on:eventname syntax instead of oneventname:
<ore-input on:change={(e) => (email = e.detail.value)} label="Email" />Angular
Standalone Components (Angular 17+)
Add CUSTOM_ELEMENTS_SCHEMA to the component decorator and import the side-effect registrations:
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@vielzeug/refine/button';
import '@vielzeug/refine/input';
@Component({
selector: 'app-contact',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<ore-input
label="Email"
[attr.value]="email"
(change)="email = $event.detail.value">
</ore-input>
<ore-button
variant="solid"
color="primary"
[attr.loading]="isLoading || null"
(click)="submit()">
Submit
</ore-button>
`,
})
export class ContactComponent {
email = '';
isLoading = false;
submit() {
this.isLoading = true;
}
}NgModule (Angular 14–16)
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
export class AppModule {}Boolean attributes
Angular's [attr.*] binding sets string values. Pass null to remove the attribute entirely:
<ore-button [attr.disabled]="isDisabled || null">Save</ore-button>SSR
Web Components use browser APIs (customElements, document, window) that do not exist in Node.js. Import Refine components only on the client.
Next.js
Use dynamic with ssr: false to ensure the import never runs on the server:
import dynamic from 'next/dynamic';
const MyForm = dynamic(() => import('./MyForm'), { ssr: false });Inside MyForm, import Refine components normally — they will only ever load in the browser.
Nuxt
Use <ClientOnly> to wrap components that depend on Refine:
<ClientOnly>
<ore-button variant="solid" color="primary">Save</ore-button>
</ClientOnly>Import Refine in a client-side plugin:
// plugins/refine.client.ts
import '@vielzeug/refine/button';
import '@vielzeug/refine/input';Generic guard
For any other environment:
if (typeof window !== 'undefined') {
import('@vielzeug/refine/button');
}