Workspace notes
Weekly design review
Next release checklist
<script setup lang="ts">
import { ref } from 'vue'
import { Plus } from '@lucide/vue'
import {
Button,
Card,
Dialog,
FloatButton,
FormField,
Heading,
Input,
Stack,
Text,
} from '@hina-ui/vue'
const open = ref(false)
const title = ref('')
const notes = ref(['Weekly design review', 'Next release checklist'])
function create() {
if (!title.value.trim()) return
notes.value.push(title.value.trim())
title.value = ''
open.value = false
}
</script>
<template>
<Card class="relative min-h-72 w-full max-w-md pb-24">
<Stack gap="lg">
<Heading :level="3" size="base">Workspace notes</Heading>
<Stack gap="sm">
<Text v-for="(note, index) in notes" :key="index" class="border-line border-b pb-3">
{{ note }}
</Text>
</Stack>
</Stack>
<FloatButton position="absolute" label="New note" @click="open = true"><Plus /></FloatButton>
<Dialog v-model:open="open" title="New note" description="Give your new note a title.">
<template #content>
<FormField label="Note title">
<Input
v-model="title"
placeholder="For example: interaction details"
@keydown.enter.prevent="create"
/>
</FormField>
</template>
<template #footer>
<Button variant="soft" tone="neutral" @click="open = false">Cancel</Button>
<Button :disabled="!title.trim()" @click="create">Create</Button>
</template>
</Dialog>
</Card>
</template>
Usage
import { FloatButton } from '@hina-ui/vue'
The button defaults to the logical bottom-end corner of the viewport. Provide an action name through label and an icon through the default slot. Use it for common actions such as creating content or opening help. Keep local actions in regular Button or IconButton controls.
<FloatButton label="New note" @click="createNote"><Plus /></FloatButton>
For a page-level action, mount it near the outer application shell so transformed ancestors do not change its fixed-position containing block. The component stays in place rather than teleporting to body, preserving its local theme, direction, and component context. The example above uses position="absolute" to keep the action inside its preview panel.
Examples
Appearance
Choose solid, soft, or outline styling. shape="square" uses a rounded square. Icon buttons reuse Tooltip when a TooltipProvider is available; AppShell already includes one. The accessible label is always present, independently of the tooltip.
Hover or focus with the keyboard to see each action name
<script setup lang="ts">
import { ref } from 'vue'
import { HelpCircle, Pencil, Plus } from '@lucide/vue'
import { FloatButton, Flex, Stack, Text } from '@hina-ui/vue'
const result = ref('Hover or focus with the keyboard to see each action name')
</script>
<template>
<Stack align="center" gap="lg">
<Flex wrap justify="center" gap="lg">
<FloatButton
position="static"
label="Create project"
@click="result = 'Selected: Create project'"
>
<Plus />
</FloatButton>
<FloatButton
position="static"
label="Edit project"
variant="soft"
shape="square"
@click="result = 'Selected: Edit project'"
>
<Pencil />
</FloatButton>
<FloatButton
position="static"
label="Help center"
variant="outline"
tone="neutral"
@click="result = 'Selected: Help center'"
>
<HelpCircle />
</FloatButton>
</Flex>
<Text role="status" size="sm" tone="muted">{{ result }}</Text>
</Stack>
</template>
Size and visible labels
sm, md, and lg have default heights of 40, 48, and 56px. extended displays the label beside the icon and suppresses the duplicate tooltip. Long labels truncate within the available width.
Extended buttons keep the same height and grow to fit the label
<script setup lang="ts">
import { ref } from 'vue'
import { Plus } from '@lucide/vue'
import { FloatButton, Flex, Stack, Text } from '@hina-ui/vue'
const result = ref('Extended buttons keep the same height and grow to fit the label')
</script>
<template>
<Stack align="center" gap="lg">
<Flex wrap align="center" justify="center" gap="lg">
<FloatButton
v-for="size in ['sm', 'md', 'lg'] as const"
:key="size"
position="static"
:size="size"
:label="`Create project(${size})`"
@click="result = `Selected: ${size}`"
>
<Plus />
</FloatButton>
<FloatButton
position="static"
extended
label="Create project"
@click="result = 'Selected: Create project'"
>
<Plus />
</FloatButton>
</Flex>
<Text role="status" size="sm" tone="muted">{{ result }}</Text>
</Stack>
</template>
Position and direction
placement uses logical directions: start and end swap in RTL. absolute positions relative to the nearest positioned ancestor. static participates in normal layout, allowing composition with Stack. offset sets the edge spacing, and { x, y } sets the horizontal and vertical spacing separately; fixed positioning also respects device safe areas.
Start and end follow the writing direction
<script setup lang="ts">
import { ref } from 'vue'
import { Plus } from '@lucide/vue'
import { Card, Center, FloatButton, FormField, Stack, Switch, Text } from '@hina-ui/vue'
const rtl = ref(false)
const result = ref('Start and end follow the writing direction')
const placements = ['top-start', 'top-end', 'bottom-start', 'bottom-end'] as const
</script>
<template>
<Stack class="w-full max-w-md">
<FormField label="Right to left" orientation="horizontal"><Switch v-model="rtl" /></FormField>
<Card :dir="rtl ? 'rtl' : 'ltr'" class="relative h-64" :padded="false">
<Center class="h-full px-16">
<Text role="status" size="sm" tone="muted" class="text-center">{{ result }}</Text>
</Center>
<FloatButton
v-for="placement in placements"
:key="placement"
position="absolute"
:placement="placement"
:label="placement"
:offset="16"
variant="soft"
size="sm"
@click="result = placement"
>
<Plus />
</FloatButton>
</Card>
</Stack>
</template>
States and visibility
visible controls enter and exit transitions. An exiting button is inert. loading keeps its dimensions and prevents repeated actions, while disabled makes it unavailable.
Visibility changes include a transition
<script setup lang="ts">
import { ref } from 'vue'
import { RefreshCw } from '@lucide/vue'
import { Center, FloatButton, FormField, Stack, Switch, Text } from '@hina-ui/vue'
const visible = ref(true)
const loading = ref(false)
const disabled = ref(false)
const result = ref('Visibility changes include a transition')
</script>
<template>
<Stack class="w-full max-w-xs">
<FormField label="Show button" orientation="horizontal"><Switch v-model="visible" /></FormField>
<FormField label="Loading" orientation="horizontal"><Switch v-model="loading" /></FormField>
<FormField label="Disabled" orientation="horizontal"><Switch v-model="disabled" /></FormField>
<Center class="h-24">
<FloatButton
position="static"
label="Sync profile"
:visible="visible"
:loading="loading"
:disabled="disabled"
@click="result = 'Action: Sync profile'"
>
<RefreshCw />
</FloatButton>
</Center>
<Text role="status" size="sm" tone="muted" class="text-center">{{ result }}</Text>
</Stack>
</template>
In-app navigation
Set as to your router’s link component. Attributes such as to pass through to the action, preserving SPA navigation.
<FloatButton :as="NuxtLink" :to="localePath('/components')" label="Component documentation">
<BookOpen />
</FloatButton>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | Required | Accessible action name and tooltip text |
visible | boolean | true | Show the button with visibility transitions |
position | 'fixed' | 'absolute' | 'static' | 'fixed' | Positioning mode |
placement | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'bottom-end' | Corner; ignored in static layout |
offset | number | string | { x?: number | string; y?: number | string } | 6 × spacing | Edge spacing; numbers are pixels. The object form sets horizontal x and vertical y separately, and an omitted axis uses the default. Fixed positioning uses the greater of spacing and safe-area inset |
size | 'sm' | 'md' | 'lg' | 'md' | Button size |
shape | 'circle' | 'square' | 'circle' | Circular or rounded-square shape |
extended | boolean | false | Show the label beside the icon |
variant | 'solid' | 'soft' | 'outline' | 'solid' | Appearance |
tone | 'accent' | 'neutral' | 'danger' | 'accent' | Color tone |
tooltip | boolean | true | Show a tooltip in icon-only mode |
tooltipSide | 'top' | 'right' | 'bottom' | 'left' | 'top' | Preferred tooltip side |
loading | boolean | false | Show progress and block activation |
disabled | boolean | false | Disable the action |
ripple | boolean | true | Ripple feedback |
as | string | Component | 'button' | Action element or component |
type | 'button' | 'submit' | 'reset' | 'button' | Native button type |
class | string | — | Button classes |
style | StyleValue | — | Button styles |
Native attributes and listeners, such as id, form, and @click, pass through to the button.
Slots
| Slot | Description |
|---|---|
default | Action icon; displayed before the label when extended |
Expose
| Name | Type | Description |
|---|---|---|
element | HTMLElement | undefined | Current action element |
focus | () => void | Focus the enabled button |