3

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?

5
  • Have you tried just inspecting a FormKit button in your project and seeing what classes it uses, and then adding those classes to your router-link? Commented Mar 12 at 9:20
  • @paddyfields yes, however, formkit adds a LOT of classes. I don't want to copy them only to have the theme change and these link buttons not change with it.
    – Soviut
    Commented Mar 12 at 9:23
  • you don't need to style the router-link itself. router-link can wrap other elements, such as a FormKit button, turning them into links. alternatively, the button can be given a onclick event listener, and you can trigger navigation programmatically.
    – yoduh
    Commented Mar 12 at 13:19
  • 1
    @yoduh neither of those are good solutions. Buttons semantically are not allowed inside links and having to use JS to handle link behaviour is bad for accessibility. I need a link that's styled, not a button that behaves like a link.
    – Soviut
    Commented Mar 12 at 17:20
  • Just a heads up — FormKit should be shipping default .formkit-${sectionName} classes in addition to the Tailwind classes if you're using the Regenesis theme from themes.formkit.com. also, from any node's context object you can get the classes object and apply them how you'd like. so classes.outer will be that node's outer section class list.
    – Boyd.dev
    Commented Mar 20 at 19:59

1 Answer 1

1
+200

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!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.