A project I inherited is using Vue 3 and FormKit for Vue and Tailwind. I would like to make some links that are styled to look like buttons from the FormKit theme. For example, a back/cancel button on the form would be a router-link, not a button or submit, but I want it to look like the other buttons.
The reason this is difficult is because FormKit has a theming system that uses Tailwind and it applies dozens of classes at run time. For example
<FormKit type="button" />
Will generate the following
<button type="button" class="inline-block bg-primary-500 text-white ...">
There are no global styles like a formkit-button
and there doesn't seem to be an easy way to get these generated styles and apply them to non <FormKit>
elements.
Copying and pasting the generated classes is out of the question because any changes to the theme will not carry over.
Is there any way to either apply FormKit button styles to a router-link, or tell the FormKit button component to render as a router-link?
You can get the classes by passing a mock component to the rootClasses
function, which is exported by the theme file (typically formkit.theme.[js|ts]
):
import { rootClasses } from "./formkit.theme" // <---- adjust to location of your theme file
const mockButton = { props: { family: 'button', type: 'button' } } as unknown as FormKitNode // remove the `as ...` if not typescript
const buttonClasses = rootClasses('input', mockButton)
This is how Forkit gets the classes, except that it uses the actual nodes. The returned object looks like this:
{
"appearance-none": true,
"[color-scheme:light]": true,
...
}
You can use it directly in the :class
prop or turn it into a list of strings when necessary:
const classString = Object.keys(buttonClasses).filter(key => buttonClasses[key]).join(' ')
You can also inject the rootClasses
through the formkit config:
import { configSymbol } from "@formkit/vue";
const config = inject(configSymbol)
config?.rootClasses(...)
But you'll probably want to keep the formkit element structure, as it impacts appearance. The straight-forward approach would be to override the element of a FormKit button using sections-schema:
<FormKit
type="button"
label="My Link"
:sections-schema="{
input: { $el: 'a' },
}"
href="..."
/>
Now an <a>
is rendered instead of a <button>
. This works without fumbling around with the rootClasses
, but not with components like RouterLink, and it will also put the button attributes (like type="button"
) on the anchor, and you'll have to add the :section-schema
prop on every link button.
To use a component, you can define a custom input, where you set your own template and register it with formkit. When setting family: button
, most (but annoyingly not all) button classes are inherited. Here is an example:
// formkit.config.ts
import { defaultConfig } from "@formkit/vue";
import { rootClasses } from "./formkit.theme";
import { createInput } from '@formkit/vue'
const buttonFamilyLink = createInput({
$cmp: 'RouterLink', // render a component
props: {
class: '$classes.input', // use the classes for the 'input' section
},
children: '$text', // put content of `text` prop into link
bind: '$attrs', // inherit attributes (like href, target, etc.)
}, {
family: 'button', // inherit button styles
props: ['text'], // register new `text` prop on FormKit component
})
export default defaultConfig({
config: {
rootClasses,
},
inputs: {
buttonFamilyLink // register new input
}
});
Now you can use it through the FormKit
component:
<FormKit
type="buttonFamilyLink"
to="..."
text="My Link"
/>
Internally, formkit passes the component to rootClasses
, which uses the family
and type
props to resolve the classes (you can explore this in your template file). But since type
is not "button"
anymore, those classes (for background and hover) are missing.
Still, this is probably the "cleanest" approach, i.e. without using rootClasses
, but it needs manual adjustment with the missing classes.
To get all button
classes, you have to apply them manually, using rootClasses
as described in the beginning. Here is an example with RouterLink
:
const routerLink = createInput({
props: {
ctx: '$node.context', // pass node context to inner component
rootClasses: '$node.config.rootClasses', // rootClasses is also available on the node
},
$cmp: {
props: ['ctx', 'rootClasses'],
setup(props) {
const linkProps = {
...props.ctx.attrs,
class: props.rootClasses('input', mockButton), // set the classes retrieved from `rootClasses`
}
const children = props.ctx.text
return () => h(RouterLink, linkProps, children)
}
},
}, {
props: ['text'] // register new `text` prop on FormKit component
})
This can be registered and used as above.
Here is a sandbox with the examples. Hope it helps!