Composables
Composition API helpers that don't belong to one component.
usePageMeta
Keeps document.title (and the favicon) in sync with reactive state. Call it once per page component; it watches the function you pass and re-applies the result whenever a dependency changes.
<script setup>
import { ref } from 'vue'
import { usePageMeta } from 'frappe-ui'
const title = ref('Loading…')
usePageMeta(() => ({
title: title.value,
emoji: '🌈',
}))
</script>The function returns null/undefined to skip an update, or a PageMeta object:
title— setsdocument.title.icon— a URL to a favicon image.emoji— renders the emoji as the favicon, taking priority overicon.- Omitting both
iconandemojirestores the page's original favicon.
Called inside a component, the watcher stops automatically on unmount — there is nothing to clean up. Called outside one (a router hook, a plain module), it returns a stop function:
const stop = usePageMeta(() => ({ title: 'Loading…' }))
// later
stop()useColorScheme
The app's light/dark preference, shared by every caller. The first call restores the saved preference and starts following the OS setting — there is nothing to install.
<script setup>
import { useColorScheme } from 'frappe-ui'
const { colorScheme, setColorScheme, toggleColorScheme } = useColorScheme()
</script>
<template>
<button @click="toggleColorScheme">
{{ colorScheme }}
</button>
</template>colorScheme—Ref<'light' | 'dark' | 'system'>, read-only. It, the<html data-theme>attribute, and the saved preference always move together, so write throughsetColorSchemerather than assigning the ref.setColorScheme(scheme)— selects a preference: appliesdata-themeand persists it.toggleColorScheme()— flips between light and dark.
Switching schemes would otherwise cross-fade every transitioning surface at once, which reads as a flash. To suppress it, useColorScheme puts a no-transition class on <html> for the two frames around the swap; the rule that acts on it ships in frappe-ui/style.css. Apps that don't load that stylesheet still switch correctly — they just see the cross-fade.
shellScrollContainer / useShellScrolled
shellScrollContainer is a computed ref pointing at the scroll element of the mounted DesktopShell or MobileShell — null when neither is mounted. Read scrollTop off it, or call scrollTo/scrollBy directly, from anywhere — a component, a vue-router scrollBehavior, a navigation guard.
<script setup>
import { shellScrollContainer } from 'frappe-ui'
function scrollToTop() {
shellScrollContainer.value?.scrollTo({ top: 0, behavior: 'smooth' })
}
</script>useShellScrolled tracks whether that container is scrolled past a threshold — the usual driver of a header's border or shadow appearing on scroll. It follows the active shell automatically across a desktop/mobile layout swap.
<script setup>
import { useShellScrolled } from 'frappe-ui'
const scrolled = useShellScrolled({ threshold: 12 })
</script>
<template>
<header :data-scrolled="scrolled">…</header>
</template>threshold defaults to 200. Without a mounted shell, scrolled stays false and the composable warns once in development.
useShortcut
Registers a global keyboard shortcut for as long as the calling component is mounted — no manual keydown listener, no cleanup to remember.
<script setup>
import { useShortcut } from 'frappe-ui'
useShortcut({
key: 'k',
ctrl: true,
description: 'Open command palette',
group: 'General',
handler: () => open(),
})
</script>Full option reference, plus getActiveShortcuts and formatShortcutLabel, are documented on KeyboardShortcutsModal — the component that renders whatever useShortcut has registered as a searchable cheat sheet.