Buttons and Inputs
Buttons, links and form inputs. Import the components from @/afcl, see AdminForth Components Library.
Button
import { Button } from '@/afcl'
<div class="flex flex-wrap gap-3">
<Button @click="doSmth">
Your button text
</Button>
<Button @click="doSmth" :loader="true">
Your button text
</Button>
</div>
loader prop would show loader when it's true.
Variant
The variant prop controls the button style. Possible values are primary (default), secondary, danger, danger-soft and ghost. danger-soft is a quieter destructive action with a red outline, e.g. Remove on a card next to secondary buttons. A ghost button has no border and no background until hover, use it for quiet actions like icons in table rows.
import { Button } from '@/afcl'
<div class="flex flex-wrap gap-3">
<Button @click="doSmth">
Primary button
</Button>
<Button @click="doSmth" variant="secondary">
Secondary button
</Button>
<Button @click="doSmth" variant="danger">
Danger button
</Button>
<Button @click="doSmth" variant="danger-soft">
Danger soft button
</Button>
<Button @click="doSmth" variant="ghost">
Ghost button
</Button>
</div>
☝️ The
modeprop is deprecated, usevariantinstead.
Size
The size prop is md (default, 34px high) or sm (28px high).
import { Button } from '@/afcl'
<div class="flex flex-wrap items-center gap-3">
<Button variant="secondary">
Medium button
</Button>
<Button variant="secondary" size="sm">
Small button
</Button>
</div>
Icon only
iconOnly makes a square button for one icon from the slot and sizes the icon to 16px. With loader the spinner takes the place of the icon.
import { Button } from '@/afcl'
import { IconCheck, IconX, IconPencil, IconTrash2 } from '@iconify-prerendered/vue-lucide'
<div class="flex flex-wrap items-center gap-3">
<Button iconOnly>
<IconCheck />
</Button>
<Button variant="secondary" iconOnly>
<IconX />
</Button>
<Button iconOnly :loader="true">
<IconCheck />
</Button>
<Button variant="ghost" size="sm" iconOnly>
<IconPencil />
</Button>
<Button variant="ghost" size="sm" iconOnly>
<IconTrash2 />
</Button>
</div>
Button Group
With active button
import { ref } from 'vue'
import { ButtonGroup } from '@/afcl'
import { IconCircleUser, IconLayoutGrid } from '@iconify-prerendered/vue-lucide'
const activeButton = ref('Profile')
<ButtonGroup v-model="activeButton">
<template #button:Profile>
<div class="flex items-center gap-2 px-4 py-2">
<IconCircleUser class="w-4 h-4" />
Profile
</div>
</template>
<template #button:Dashboard>
<div class="flex items-center gap-2 px-4 py-2">
<IconLayoutGrid class="w-4 h-4" />
Board
</div>
</template>
</ButtonGroup>
With solid color
import { ButtonGroup } from '@/afcl'
import { IconPlus, IconChevronDown } from '@iconify-prerendered/vue-lucide'
<ButtonGroup :solidColor="true">
<template #button:Add>
<div class="flex items-center gap-2 px-4 py-2">
<IconPlus class="w-4 h-4" />
Add Local Passkey
</div>
</template>
<template #button:Dropdown>
<div class="flex items-center px-2 py-2">
<IconChevronDown class="w-4 h-4" />
</div>
</template>
</ButtonGroup>
Link
import { Link } from '@/afcl'
<Link to="/login">Go to login</Link>
A to with a scheme (https://, mailto:, tel:, otpauth:) renders a plain link which leaves the app, other values are routes.
LinkButton
Looks like button but works like link. Uses router-link under the hood. Takes the same variant, size and iconOnly props as Button.
import { LinkButton } from '@/afcl'
<LinkButton to="/login">Go to login</LinkButton>
Select
Single
import { ref } from 'vue'
import { Select } from '@/afcl'
const selected = ref(null)
<Select
class="w-full"
:options="[
{label: 'Last 7 days', value: '7'},
{label: 'Last 30 days', value: '30'},
{label: 'Last 90 days', value: '90'},
{label: 'None', value: null}
]"
v-model="selected"
></Select>
Multiple
import { Select } from '@/afcl'
import { ref } from 'vue'
const selected = ref(null)
const selected = ref([])
<Select
class="w-full"
:options="[
{label: 'Last 7 days', value: '7'},
{label: 'Last 30 days', value: '30'},
{label: 'Last 90 days', value: '90'},
{label: 'None', value: null}
]"
v-model="selected"
multiple
></Select>
Custom slots for item
You can customize item and selected item using slots.
import { Select } from '@/afcl'
import { ref } from 'vue'
const selected = ref(null)
<Select
class="w-full"
:options="[
{label: 'Last 7 days', value: '7', records: 110},
{label: 'Last 30 days', value: '30', records: 320},
{label: 'Last 90 days', value: '90', records: 310},
{label: 'None', value: null}
]"
v-model="selected"
>
<template #item="{option}">
<div>
<span>{{ option.label }}</span>
<span class="ml-2 opacity-50">{{ option.records }} records</span>
</div>
</template>
<template #selected-item="{option}">
<span>{{ option.label }} 💫</span>
</template>
</Select>
Extra item
You might need to put some extra item at bottom of list
import { Select, LinkButton } from '@/afcl'
import { ref } from 'vue'
const selected = ref(null)
<Select
class="w-full"
:options="[
{label: 'Last 7 days', value: '7'},
{label: 'Last 30 days', value: '30'},
{label: 'Last 90 days', value: '90'},
]"
v-model="selected"
>
<template #extra-item>
<LinkButton to="/ranges">Manage ranges</LinkButton>
</template>
</Select>
Add classes to the input directly
Pass classesForInput to style the input of the select.
import { Select } from '@/afcl'
import { ref } from 'vue'
const selected = ref(null)
<Select
class="w-full"
:options="[
{label: 'Last 7 days', value: '7'},
{label: 'Last 30 days', value: '30'},
{label: 'Last 90 days', value: '90'},
]"
v-model="selected"
classesForInput="py-[4px] text-sm bg-white rounded"
></Select>
Input
import { Input } from '@/afcl'
<Input type="number" class="w-full">
<template #suffix>
USD
</template>
</Input>
import { Input } from '@/afcl'
import { IconSearch } from '@iconify-prerendered/vue-lucide'
<Input type="text" class="w-full">
<template #rightIcon>
<IconSearch class="w-4 h-4 text-lightInputIconColor dark:text-darkInputIconColor" />
</template>
</Input>
passwordToggle adds a button which shows and hides the value of a type="password" input, invalid paints the border red:
import { Input } from '@/afcl'
<div class="flex flex-col gap-3">
<Input type="password" passwordToggle fullWidth modelValue="correct horse" />
<Input type="password" passwordToggle invalid fullWidth modelValue="short" />
</div>
Code Input
An input for one-time codes, e.g. a 6-digit code from an authenticator app. It is one real field drawn as cells, so password managers and the browser fill the whole code at once, a pasted code with spaces or a dash (123 456) is cleaned, and screen readers read one field. Only digits are kept, complete is emitted when the last digit is entered.
import { ref } from 'vue'
import { CodeInput } from '@/afcl'
const code = ref('')
const checked = ref('')
<div class="flex flex-col gap-3">
<CodeInput v-model="code" :groupSize="3" @complete="checked = $event" />
<p class="text-sm">Completed code: {{ checked || '-' }}</p>
</div>
invalid paints the cells red, e.g. after a wrong code:
import { CodeInput } from '@/afcl'
<CodeInput modelValue="1234" :groupSize="3" invalid />
Props:
length?: number // number of digits, 6 by default
groupSize?: number // draws a dash after every groupSize digits, 0 (default) draws none
invalid?: boolean
disabled?: boolean
autofocus?: boolean // focuses the field once it is shown, also inside a dialog
// the code is the main control of the screen or dialog: after a click anywhere, e.g. on a copy button,
// the focus comes back to it, Tab still moves on, and a code pasted anywhere on the page fills it
keepFocus?: boolean
Attributes like id, name and aria-labelledby go to the field. Through a template ref the component exposes focus() and clear(), which empties the field and focuses it, e.g. after the server rejected the code.
Textarea
import { ref } from 'vue'
import { Textarea } from '@/afcl'
const textAreaInput = ref('')
<Textarea
v-model="textAreaInput"
placeholder="Enter some text..."
class="w-64"
/>
Checkbox
import { ref } from 'vue'
import { Checkbox } from '@/afcl'
const enable = ref(false)
<Checkbox v-model="enable">
Enable
</Checkbox>
Toggle
import { ref } from 'vue'
import { Toggle } from '@/afcl'
const enabled = ref(false)
<Toggle v-model="enabled" :disabled="false">
<p>Click me</p>
</Toggle>
Date picker
Basic
import { ref } from 'vue'
import { DatePicker } from '@/afcl';
const datePickerValue = ref()
<DatePicker
v-model:valueStart="datePickerValue"
:column="{ type: 'datetime' }"
label="Pick start"
/>
Dropzone
import { ref, watch } from 'vue'
import { Dropzone } from '@/afcl'
const files = ref<File[]>([])
watch(files, (files) => {
console.log('files selected', files);
})
<Dropzone
:extensions="['.jpg', '.jpeg', '.png']"
:maxSizeBytes="1024 * 1024 * 2"
:multiple="false"
v-model="files"
/>