Opticians and frame makers
Frames cut by hand from single sheets of acetate, and eye tests that take the full hour. Come in and see the world in sharper lines.
A split hero with a large sans headline, an intro, an Arrow Pill and an underline link beside the op-art background component, a live WebGL field in the spirit of Bridget Riley: dense parallel lines ripple in slow waves and swell from thin to heavy across the canvas, bulging around the cursor or a wandering focus. Every field setting passes through the art prop; ink and paper follow the theme, lines stay crisp at any size, it pauses offscreen, holds a still frame for reduced motion and falls back to CSS stripes without WebGL.
A split hero with a large sans headline, an intro and two buttons on the left, and a live WebGL field from the op-art component on the right: dense parallel lines that ripple in slow waves, swell from thin to heavy and bulge around the cursor or a wandering focus.
Upgrade to Pro
Pro blocks install through the shadcn CLI with your license key and ship their full source. Docs and live previews stay open to everyone, so you can read every block's details first.
Swap YOUR_EMAIL and YOUR_KEY for the email and license key on your account. Find your license key on your account page.
Radix flavor
npx shadcn add "https://ui.beste.co/r/hero201?email=YOUR_EMAIL&license_key=YOUR_KEY"Base UI flavor
npx shadcn add "https://ui.beste.co/r-base/hero201?email=YOUR_EMAIL&license_key=YOUR_KEY"This installs the block to components/beste/block/hero201.tsx plus the op-art background and the button23 and button2 buttons it uses.
The installed file exports hero201Demo alongside the block: the exact props behind the preview above. Spread it to get a working hero in one line.
import { Hero201, hero201Demo } from "@/components/beste/block/hero201";
export default function Page() {
return <Hero201 {...hero201Demo} />;
}Then replace the demo with your own props. Written out, a trimmed setup looks like this:
import { Hero201 } from "@/components/beste/block/hero201";
export default function Page() {
return (
<Hero201
eyebrow="Opticians and frame makers"
heading="Look closer. It moves when you do."
description="Frames cut by hand from single sheets of acetate, and eye tests that take the full hour."
buttons={[
{ label: "Book an eye test", href: "/book" },
{ label: "See the frames", href: "/frames" },
]}
art={{ inkColor: "var(--foreground)", paperColor: "var(--background)", lines: 34, pull: 0.5 }}
/>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
eyebrow | string | – | Small line above the heading |
heading | string | – | Headline, rendered as the page h1 |
description | string | – | Intro under the heading |
buttons | ActionButton[] | [] | The first renders as a dark Button23 arrow pill, the rest as Button2 underline links |
art | Omit<OpArtProps, "className" | "children"> | – | Forwards the OpArt component's props: inkColor, paperColor, lines, angle, wave, weight, contrast, speed, interactive, pull, paused |
className | string | – | Extra classes for the outer <section> |
type ActionButton = {
label: string;
href: string;
};lg the text takes 5 of 12 columns and the field 7, at least max(640px, 100svh - 78px) tall; below lg the field stacks under the text at 4/3, then 16/10 from sm.background softens the seam between text and field on large screens.inkColor and paperColor accept CSS variables, so the demo's var(--foreground) and var(--background) follow the theme in light and dark mode.