Using Flint UI with Next.js ​
This guide covers how to use @getufy/flint-ui-react in a Next.js application with server-side rendering (SSR).
Installation ​
npm install @getufy/flint-ui @getufy/flint-ui-reactThe 'use client' Directive ​
Web components rely on browser APIs (customElements, HTMLElement, shadow DOM) that are not available during server-side rendering. All components from @getufy/flint-ui-react must be used inside Client Components.
Add the 'use client' directive at the top of any file that imports Flint UI components:
'use client';
import { FlintButton } from '@getufy/flint-ui-react/button';
export function MyComponent() {
return <FlintButton variant="contained">Click me</FlintButton>;
}Why is 'use client' required? ​
Next.js App Router renders components as Server Components by default. Server Components execute on the server where customElements, HTMLElement, and the shadow DOM API do not exist. The 'use client' directive tells Next.js to render the component in the browser, where web component APIs are available.
Any component that imports a 'use client' module automatically becomes a Client Component itself, so you only need the directive in the file that directly imports Flint UI.
Client-Only Wrapper Pattern ​
Instead of adding 'use client' to every file that uses Flint UI, create a single re-export file:
// components/flint-client.tsx
'use client';
// Re-export only the components your app uses (tree-shaking friendly)
export { FlintButton } from '@getufy/flint-ui-react/button';
export { FlintInput } from '@getufy/flint-ui-react/input';
export { FlintSelect, FlintOption } from '@getufy/flint-ui-react/select';
export { FlintDialog } from '@getufy/flint-ui-react/dialog';
export { FlintTabs, FlintTab, FlintTabPanel } from '@getufy/flint-ui-react/tabs';Then import from this wrapper anywhere in your app:
// app/dashboard/page.tsx (Server Component — no 'use client' needed here)
import { FlintButton, FlintInput } from '@/components/flint-client';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<SearchBar />
</div>
);
}
// This becomes a Client Component automatically because it uses FlintInput
function SearchBar() {
return <FlintInput placeholder="Search..." />;
}Dynamic Imports for SSR Safety ​
If you need to completely skip server-side rendering for a component (e.g., it measures the viewport or uses IntersectionObserver at mount time), use Next.js dynamic() with ssr: false:
import dynamic from 'next/dynamic';
const FlintCarousel = dynamic(
() => import('@getufy/flint-ui-react/carousel').then((mod) => mod.FlintCarousel),
{
ssr: false,
loading: () => <div style={{ height: 300 }}>Loading...</div>,
}
);
export default function Page() {
return <FlintCarousel>{/* slides */}</FlintCarousel>;
}This approach works for both the App Router and the Pages Router. Use it sparingly -- most Flint components work fine with 'use client' alone. Reserve dynamic() for components that:
- Access
window.innerWidth/ResizeObserverat mount time (e.g.,FlintCarousel,FlintSplitPanel) - Use
IntersectionObserverorMutationObserverimmediately - Cause hydration mismatch warnings
Theme Setup ​
App Router (app/layout.tsx) ​
Import the theme CSS in your root layout:
import '@getufy/flint-ui/theme.css';
// Optional: dark theme
// import '@getufy/flint-ui/theme-dark.css';
// Optional: color palettes
// import '@getufy/flint-ui/theme-teal.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}Pages Router (pages/_app.tsx) ​
import '@getufy/flint-ui/theme.css';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}Programmatic Theme Switching ​
Use the setFlintTheme utility for runtime theme changes:
'use client';
import { setFlintTheme } from '@getufy/flint-ui';
export function ThemeToggle() {
return (
<button onClick={() => setFlintTheme('dark')}>
Toggle Dark Mode
</button>
);
}Event Handling ​
React wrappers from @getufy/flint-ui-react map custom events to React-style callbacks:
'use client';
import { FlintInput } from '@getufy/flint-ui-react/input';
export function SearchInput() {
return (
<FlintInput
placeholder="Search..."
onFlintInputChange={(e) => console.log(e.detail.value)}
/>
);
}Suppress Lit Dev Warnings (Optional) ​
Lit emits development-mode warnings in the browser console. To suppress them:
// app/layout.tsx or pages/_app.tsx
import '@getufy/flint-ui/suppress-warnings';Common Pitfalls ​
Hydration mismatch ​
Web components render as empty <flint-button></flint-button> tags on the server. On the client, the custom element upgrades and renders its shadow DOM. This mismatch can cause React hydration warnings. Solutions:
- Use
'use client'(recommended) -- the component only renders on the client after hydration - Use
dynamic(..., { ssr: false })-- completely skip SSR for that component - Provide a
loadingfallback withdynamic()to avoid layout shift
Missing styles ​
Ensure the theme CSS is imported in a layout or _app file, not inside a client component that may be lazy-loaded:
// app/layout.tsx -- correct: imported at the root
import '@getufy/flint-ui/theme.css';Custom element registration timing ​
customElements.define() runs when a component module is first imported. If the HTML tag <flint-button> appears in the DOM before the module loads, it renders as an unknown element until the definition is registered. This is normal and expected -- the element "upgrades" once the import executes.
To avoid a flash of unstyled content (FOUC), ensure component imports are in your app bundle (not lazy-loaded after render):
'use client';
// These imports register the custom elements synchronously
import { FlintButton } from '@getufy/flint-ui-react/button';
import { FlintInput } from '@getufy/flint-ui-react/input';window / document access ​
All Flint UI components guard browser API access inside lifecycle methods (connectedCallback, firstUpdated, etc.), so importing them at the module level is safe on the server. However, do not call component methods or access DOM properties outside of React effects:
'use client';
import { useRef, useEffect } from 'react';
import { FlintDialog } from '@getufy/flint-ui-react/dialog';
function MyDialog() {
const ref = useRef(null);
useEffect(() => {
// Safe: runs only on the client
ref.current?.show();
}, []);
return <FlintDialog ref={ref}>Content</FlintDialog>;
}TypeScript ​
Event types are exported from @getufy/flint-ui (e.g., FlintInputChangeEvent) for type-safe event handlers:
import type { FlintSelectChangeEvent } from '@getufy/flint-ui';
<FlintSelect onFlintSelectChange={(e: FlintSelectChangeEvent) => {
console.log(e.detail.value);
}} />