Rendered from the shared Frontfriend stories with React Base, the same
preview Studio shows. Styling comes from your Design System; the code below
is native to Reka UI.
Installation
Complete project setup first. Your configuration
must select vue/reka and include your Design System ID.
pnpm exec frontfriend download page -f vue --primitive reka
Review dependencies reported by the downloader and keep your source
changes in version control.
Usage
<script setup lang="ts">
import { Page } from "@/components/ui/page";
import { PageContainer } from "@/components/ui/page";
import { PageContent } from "@/components/ui/page";
import { PageContentMain } from "@/components/ui/page";
const args = {
"content": "This is a page",
"layout": "main",
} satisfies Record<string, unknown> & {
layout?:
| "main"
| "inset"
| "focus"
| "focus-inset"
| "inset-max-w"
| "inset-no-bg"
| "inset-2col";
};
</script>
<template>
<Page :layout="args.layout"
><PageContainer
><PageContent
><PageContentMain>{{ args.content }}</PageContentMain></PageContent
></PageContainer
></Page
>
</template>
Examples
Main
<script setup lang="ts">
import { Page } from "@/components/ui/page";
import { SidebarProvider } from "@/components/ui/sidebar";
import { defineComponent } from "vue";
import { Sidebar } from "@/components/ui/sidebar";
import { SidebarContent } from "@/components/ui/sidebar";
import { SidebarFooter } from "@/components/ui/sidebar";
import { SidebarHeader } from "@/components/ui/sidebar";
import { SidebarMenu } from "@/components/ui/sidebar";
import { SidebarMenuButton } from "@/components/ui/sidebar";
import { SidebarMenuItem } from "@/components/ui/sidebar";
import { Icon } from "@/components/ui/icon";
import { Avatar } from "@/components/ui/avatar";
import { AvatarFallback } from "@/components/ui/avatar";
import { AvatarImage } from "@/components/ui/avatar";
import { DropdownMenu } from "@/components/ui/dropdown-menu";
import { DropdownMenuContent } from "@/components/ui/dropdown-menu";
import { DropdownMenuGroup } from "@/components/ui/dropdown-menu";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { DropdownMenuLabel } from "@/components/ui/dropdown-menu";
import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { useSidebar } from "@/components/ui/sidebar";
import { Toast } from "@/components/ui/toast";
import { Dialog } from "@/components/ui/dialog";
import { DialogBody } from "@/components/ui/dialog";
import { DialogContent } from "@/components/ui/dialog";
import { DialogFooter } from "@/components/ui/dialog";
import { DialogHeader } from "@/components/ui/dialog";
import { DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { FormControl } from "@/components/ui/form";
import { FormField } from "@/components/ui/form";
import { FormItem } from "@/components/ui/form";
import { FormLabel } from "@/components/ui/form";
import { FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PhoneInput } from "@/components/ui/phone-input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { reactive } from "vue";
import { ref } from "vue";
import { toTypedSchema } from "@vee-validate/zod";
import { z } from "zod";
import { useForm } from "vee-validate";
import { toast } from "vue-sonner";
import { watch } from "vue";
import { SidebarInset } from "@/components/ui/sidebar";
import { PageWrap } from "@/components/ui/page";
import { Navbar } from "@/components/ui/navbar";
import { PageInner } from "@/components/ui/page";
import { PageContainer } from "@/components/ui/page";
import { PageContent } from "@/components/ui/page";
import { PageContentMain } from "@/components/ui/page";
const AppSidebarLayout = defineComponent({
components: {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
Icon,
NavUserLayout: {
components: {
Avatar,
AvatarFallback,
AvatarImage,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
Icon,
Toast,
Dialog,
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Button,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
PhoneInput,
Label,
Separator,
},
setup() {
const user = reactive({
name: "Gustavo",
email: "m@example.com",
avatar:
"https://uploads.donux.com/api/image/67d2ebb62bd10db0fd32e15c",
phone: "+39 333 1234567",
});
const { isMobile } = useSidebar();
const nameValue = ref(user.name);
const emailValue = ref(user.email);
const phoneValue = ref(user.phone);
const currentPasswordValue = ref("");
const newPasswordValue = ref("");
const confirmPasswordValue = ref("");
const showPersonalInfoDialog = ref(false);
const showNameChangeDialog = ref(false);
const showEmailChangeDialog = ref(false);
const showPhoneChangeDialog = ref(false);
const showPasswordChangeDialog = ref(false);
const nameFormSchema = toTypedSchema(
z.object({
name: z.string().min(2).max(50),
}),
);
const emailFormSchema = toTypedSchema(
z.object({
email: z.string().email("Please enter a valid email address"),
}),
);
const phoneFormSchema = toTypedSchema(
z.object({
phone: z.string().min(5, "Please enter a valid phone number"),
}),
);
const passwordFormSchema = toTypedSchema(
z
.object({
currentPassword: z
.string()
.min(4, "Password must be at least 4 characters"),
newPassword: z
.string()
.min(4, "Password must be at least 4 characters"),
confirmPassword: z
.string()
.min(4, "Password must be at least 4 characters"),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
}),
);
const nameForm = useForm({
validationSchema: nameFormSchema,
initialValues: {
name: user.name,
},
});
const emailForm = useForm({
validationSchema: emailFormSchema,
initialValues: {
email: user.email,
},
});
const phoneForm = useForm({
validationSchema: phoneFormSchema,
initialValues: {
phone: user.phone,
},
});
const passwordForm = useForm({
validationSchema: passwordFormSchema,
});
const handleNameSubmit = nameForm.handleSubmit((values) => {
user.name = values.name;
showNameChangeDialog.value = false;
showPersonalInfoDialog.value = true;
toast.success("Your name has been changed successfully.");
});
const handleEmailSubmit = emailForm.handleSubmit((values) => {
emailValue.value = values.email;
user.email = values.email;
showEmailChangeDialog.value = false;
showPersonalInfoDialog.value = true;
toast.success("Your email has been changed successfully.");
});
const handlePhoneSubmit = phoneForm.handleSubmit((values) => {
phoneValue.value = values.phone;
user.phone = values.phone;
showPhoneChangeDialog.value = false;
showPersonalInfoDialog.value = true;
toast.success("Your phone number has been changed successfully.");
});
const handlePasswordSubmit = passwordForm.handleSubmit((_values) => {
showPasswordChangeDialog.value = false;
toast.success("Your password has been changed successfully.");
});
function openNameDialog() {
nameValue.value = user.name;
nameForm.resetForm({
values: {
name: user.name,
},
});
showNameChangeDialog.value = true;
showPersonalInfoDialog.value = false;
}
function openEmailDialog() {
emailForm.resetForm({
values: {
email: user.email,
},
});
emailValue.value = user.email;
showEmailChangeDialog.value = true;
showPersonalInfoDialog.value = false;
}
function openPhoneDialog() {
phoneForm.resetForm({
values: {
phone: user.phone,
},
});
phoneValue.value = user.phone;
showPhoneChangeDialog.value = true;
showPersonalInfoDialog.value = false;
}
function openPasswordDialog() {
currentPasswordValue.value = "";
newPasswordValue.value = "";
confirmPasswordValue.value = "";
passwordForm.resetForm();
showPasswordChangeDialog.value = true;
showPersonalInfoDialog.value = false;
}
watch(
() => nameValue.value,
(newValue) => {
nameForm.setFieldValue("name", newValue, true);
},
);
watch(
() => emailValue.value,
(newValue) => {
emailForm.setFieldValue("email", newValue, true);
},
);
watch(
() => phoneValue.value,
(newValue) => {
phoneForm.setFieldValue("phone", newValue, true);
},
);
watch(
() => currentPasswordValue.value,
(newValue) => {
passwordForm.setFieldValue("currentPassword", newValue, true);
},
);
watch(
() => newPasswordValue.value,
(newValue) => {
passwordForm.setFieldValue("newPassword", newValue, true);
},
);
watch(
() => confirmPasswordValue.value,
(newValue) => {
passwordForm.setFieldValue("confirmPassword", newValue, true);
},
);
return {
user,
isMobile,
nameValue,
emailValue,
phoneValue,
currentPasswordValue,
newPasswordValue,
confirmPasswordValue,
showPersonalInfoDialog,
showNameChangeDialog,
showEmailChangeDialog,
showPhoneChangeDialog,
showPasswordChangeDialog,
handleNameSubmit,
handleEmailSubmit,
handlePhoneSubmit,
handlePasswordSubmit,
openNameDialog,
openEmailDialog,
openPhoneDialog,
openPasswordDialog,
};
},
template: `
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="group user hover:bg-neutral-subtle-hover active:bg-neutral-low-active data-[state=open]:bg-neutral-low-active data-[state=open]:text-neutral-subtle"
>
<Avatar class="h-8 w-8">
<AvatarImage :src="user.avatar" :alt="user.name" />
<AvatarFallback name="CN" class="" />
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold text-neutral-strong group-[.user]:group-hover:text-neutral-strong-hover group-[.user]:group-active:text-neutral-strong-active">{{ user.name }}</span>
<span class="truncate text-xs text-neutral-strong group-[.user]:group-hover:text-neutral-mid-hover group-[.user]:group-active:text-neutral-mid-active">{{ user.email }}</span>
</div>
<Icon name="ChevronsUpDown" class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-full min-w-56 rounded-lg z-[51]"
:side="isMobile ? 'bottom' : 'right'"
align="end"
:side-offset="4"
>
<DropdownMenuLabel>
My Profile
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem @click="showPersonalInfoDialog = true">
<Icon name="Settings" />
Settings
</DropdownMenuItem>
<DropdownMenuItem>
<Icon name="LogOut" />
Log out
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
<Dialog v-model:open="showPersonalInfoDialog">
<DialogContent width="lg" height="auto">
<DialogHeader>
<DialogTitle>Personal Information</DialogTitle>
</DialogHeader>
<DialogBody class="mb-0">
<div class="flex flex-col gap-5">
<div class="flex items-center justify-between">
<div>
<Label variant="secondary">
Full Name
</Label>
<p class="text-base font-primary text-neutral-mid">
{{ user.name }}
</p>
</div>
<Button variant="tertiary" size="sm" @click="openNameDialog">
Change
</Button>
</div>
<Separator orientation="horizontal" no-padding />
<div class="flex items-center justify-between">
<div>
<Label variant="secondary">
Email
</Label>
<p class="text-base font-primary text-neutral-mid">
{{ user.email }}
</p>
</div>
<Button variant="tertiary" size="sm" @click="openEmailDialog">
Change
</Button>
</div>
<Separator orientation="horizontal" no-padding />
<div class="flex items-center justify-between">
<div>
<Label variant="secondary">
Phone Number
</Label>
<p class="text-base font-primary text-neutral-mid">
{{ user.phone }}
</p>
</div>
<Button variant="tertiary" size="sm" @click="openPhoneDialog">
Change
</Button>
</div>
<Separator orientation="horizontal" no-padding />
<div class="flex items-center justify-between">
<div>
<Label variant="secondary">
Password
</Label>
<p class="text-base font-primary text-neutral-mid">
Change your password to access your account.
</p>
</div>
<Button variant="tertiary" size="sm" @click="openPasswordDialog">
Change
</Button>
</div>
</div>
</DialogBody>
</DialogContent>
</Dialog>
<Dialog v-model:open="showNameChangeDialog" :on-open-change="val => !val && (showPersonalInfoDialog = true)">
<DialogContent width="lg" height="auto">
<DialogHeader>
<DialogTitle>Change Name</DialogTitle>
</DialogHeader>
<form id="nameForm" @submit="handleNameSubmit">
<DialogBody class="space-y-4">
<FormField name="name">
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input
v-model="nameValue"
type="text"
placeholder="Enter your full name"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DialogBody>
<DialogFooter>
<Button type="submit" form="nameForm" size="lg">
Save
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog v-model:open="showEmailChangeDialog" :on-open-change="val => !val && (showPersonalInfoDialog = true)" >
<DialogContent width="lg" height="auto">
<DialogHeader>
<DialogTitle>Change Email</DialogTitle>
</DialogHeader>
<form id="emailForm" @submit="handleEmailSubmit">
<DialogBody class="space-y-4">
<FormField name="email">
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
v-model="emailValue"
type="email"
placeholder="Enter your email address"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DialogBody>
<DialogFooter>
<Button type="submit" form="emailForm" size="lg">
Save
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog v-model:open="showPhoneChangeDialog" :on-open-change="val => !val && (showPersonalInfoDialog = true)">
<DialogContent width="lg" height="auto">
<DialogHeader>
<DialogTitle>Change Phone Number</DialogTitle>
</DialogHeader>
<form id="phoneForm" @submit="handlePhoneSubmit">
<DialogBody class="space-y-4">
<FormField name="phone">
<FormItem>
<FormLabel>Phone Number</FormLabel>
<FormControl>
<PhoneInput
v-model="phoneValue"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DialogBody>
<DialogFooter>
<Button type="submit" form="phoneForm" size="lg">
Save
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog v-model:open="showPasswordChangeDialog" :on-open-change="val => !val && (showPersonalInfoDialog = true)">
<DialogContent width="lg" height="auto">
<DialogHeader>
<DialogTitle>Change Password</DialogTitle>
</DialogHeader>
<form id="passwordForm" @submit="handlePasswordSubmit">
<DialogBody class="space-y-4">
<FormField name="currentPassword">
<FormItem>
<FormLabel>Current Password</FormLabel>
<FormControl>
<Input
v-model="currentPasswordValue"
type="password"
placeholder="Enter current password"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField name="newPassword">
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<Input
v-model="newPasswordValue"
type="password"
placeholder="Enter new password"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField name="confirmPassword">
<FormItem>
<FormLabel>Confirm New Password</FormLabel>
<FormControl>
<Input
v-model="confirmPasswordValue"
type="password"
placeholder="Confirm new password"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DialogBody>
<DialogFooter>
<Button type="submit" form="passwordForm" size="lg">
Save
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Toast variant="success" icon="CircleCheck" />
`,
},
},
props: {
activeTab: {
type: String,
default: "tours",
},
},
setup(props) {
return { props };
},
template: `
<Sidebar collapsible="icon" class="h-full min-h-0">
<SidebarHeader>
<div class="w-[140px] h-9 flex items-center text-lg font-bold">Tinorent</div>
</SidebarHeader>
<SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton :is-active="props.activeTab === 'tours'">
<Icon name="Signpost" />
Tours
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton :is-active="props.activeTab === 'planner'">
<Icon name="Book" />
Planner
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarContent>
<SidebarFooter>
<NavUserLayout />
</SidebarFooter>
</Sidebar>
`,
});
const ToggleSidebarButton = defineComponent({
components: { FFButton: Button, Icon },
props: {
class: String,
},
template: `<FFButton variant="ghost" size="sm" :class="$props.class"><Icon name="Menu" /></FFButton>`,
});
const args = {
};
</script>
<template>
<Page>
<SidebarProvider>
<AppSidebarLayout />
<SidebarInset class="min-h-full relative">
<PageWrap>
<Navbar title="Page title" type="relaxed">
<template #left>
<ToggleSidebarButton class="inline-flex sm:hidden" />
<Button
variant="tertiary"
size="sm"
icon="ChevronLeft"
class="sm:inline-flex hidden"
/>
<Button
variant="ghost"
icon="ChevronLeft"
class="inline-flex sm:hidden"
/>
</template>
<template #right>
<Button variant="main" class="sm:inline-flex hidden">
Action
</Button>
</template>
</Navbar>
<PageInner>
<PageContainer>
<PageContent>
<PageContentMain />
</PageContent>
</PageContainer>
</PageInner>
</PageWrap>
</SidebarInset>
</SidebarProvider>
</Page>
</template>
Focus
<script setup lang="ts">
import { computed } from "vue";
import { toTypedSchema } from "@vee-validate/zod";
import { z } from "zod";
import { useForm } from "vee-validate";
import { toast } from "vue-sonner";
import { Page } from "@/components/ui/page";
import { PageWrap } from "@/components/ui/page";
import { PageInner } from "@/components/ui/page";
import { PageContainer } from "@/components/ui/page";
import { PageContent } from "@/components/ui/page";
import { PageContentMain } from "@/components/ui/page";
import { defineComponent } from "vue";
import { Card } from "@/components/ui/card";
import { CardContent } from "@/components/ui/card";
import { FormField } from "@/components/ui/form";
import { FormItem } from "@/components/ui/form";
import { FormLabel } from "@/components/ui/form";
import { FormControl } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { FormMessage } from "@/components/ui/form";
import { CheckboxField } from "@/components/ui/checkbox-field";
import { Button } from "@/components/ui/button";
import { Toast } from "@/components/ui/toast";
const Logo = defineComponent({
props: {
class: String,
},
template: `<div :class="$props.class" class="flex items-center text-lg font-bold">Tinorent</div>`,
});
const args = {
};
const formSchema = computed(() =>
toTypedSchema(
z.object({
email: z.string().min(2).max(50).email(),
password: z.string().min(4).max(50),
savePassword: z.boolean().optional(),
}),
),
).value;
const form = useForm({
validationSchema: formSchema,
});
const onSubmit = form.handleSubmit((_values) => {
toast.success("Login completed successfully.");
});
</script>
<template>
<Page layout="focus-inset">
<PageWrap layout="focus-inset">
<PageInner layout="focus-inset">
<PageContainer layout="focus-inset">
<PageContent layout="focus-inset">
<PageContentMain layout="focus-inset">
<Logo class="w-[140px] h-9" />
<Card class="w-full">
<CardContent class="max-h-[800px]">
<div
class="flex py-6 lg:py-14 px-0 lg:px-10 flex-col justify-center items-center gap-10 w-full"
>
<div class="text-3xl font-semibold leading-none">
Sign in
</div>
<form
id="mainform"
class="flex items-end gap-5 self-stretch w-full flex-col"
@submit="onSubmit"
>
<FormField v-slot="{ componentField }" name="email">
<FormItem class="w-full">
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="Enter your email here"
type="text"
v-bind="componentField"
class="w-full"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="password">
<FormItem class="w-full">
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="*******"
type="password"
v-bind="componentField"
class="w-full"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField
v-slot="{ componentField }"
name="savePassword"
>
<FormItem>
<FormControl>
<CheckboxField v-bind="componentField">
Save password
</CheckboxField>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</form>
<Button
form="mainform"
type="submit"
size="lg"
variant="main"
class="w-full"
>
Login
</Button>
<Button variant="link" size="sm" class="w-full">
Forgot password?
</Button>
</div>
</CardContent>
</Card>
</PageContentMain>
</PageContent>
</PageContainer>
</PageInner>
<Toast variant="success" icon="CircleCheck" />
</PageWrap>
</Page>
</template>
API reference
Frontfriend-owned props. Native HTML and inherited primitive props
remain documented by Reka UI.
Page
| Prop | Type | Default | Description |
class | string | — | |
layout | LayoutType | "main" | |
PageContainer
| Prop | Type | Default | Description |
class | string | — | |
PageContent
| Prop | Type | Default | Description |
class | string | — | |
PageContentMain
| Prop | Type | Default | Description |
class | string | — | |
noPadding | boolean | false | |
PageInner
| Prop | Type | Default | Description |
class | string | — | |
noPadding | boolean | false | |
PageSide
| Prop | Type | Default | Description |
class | string | — | |
PageWrap
| Prop | Type | Default | Description |
class | string | — | |
Configuration determines the actual classes and available styling hooks.
See Theming & component configuration before introducing
a repeated local override.