How to make a link button using Vue FormKit?

130 views Asked by At

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?

2

There are 2 answers

0
Moritz Ringler On BEST ANSWER

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!

3
VonC On

I need a link that's styled, not a button that behaves like a link. –

Given the additional context from the comments, the goal is to maintain semantic HTML and accessibility standards while making sure that the visual styling remains consistent with FormKit's button styles, without resorting to embedding a button within a router-link or using JavaScript to mimic link behavior.

A viable approach could be to create a custom Vue directive or a wrapper component that applies FormKit's button styles to a router-link. That way, if FormKit's theme changes, your link styled as a button will automatically inherit these changes without requiring manual updates.


You can create a directive that fetches the current FormKit button styles and applies them to any router-link.

// directive to apply FormKit button styles
Vue.directive('formkit-style', {
  mounted(el) {
    // Logic to fetch and apply FormKit button styles
    el.classList.add('formkit-button', 'formkit-button-default');
    // Add more logic if styles are dynamically generated or changed
  }
});

Usage:

<router-link to="/back" v-formkit-style>Back</router-link>

Alternatively, you can create a Vue component that renders a router-link and automatically applies the necessary FormKit classes and styles. That gives you more control over the rendering and styling.

<template>
  <router-link :to="to" class="formkit-button formkit-button-default">
    <slot></slot>
  </router-link>
</template>

<script>
export default {
  props: {
    to: {
      type: String,
      required: true
    }
  }
}
</script>

<style scoped>
/* Optionally include specific styling or adjustments here */
</style>

Usage:

<custom-router-link to="/back">Back</custom-router-link>

Isn't the directive overkill? I can already apply those classes directly to the router-link element.
The problem is that FormKit has a theming system which generates dozens of tailwind classes for each element, including buttons; there is no global formkit-button class.

The existence of a dynamic theming system complicates the direct application of styles without a straightforward, maintainable class like formkit-button.

Since the directive approach might be seen as overkill and the classes are dynamically generated, you might consider creating a set of Tailwind CSS utility classes that approximate the appearance of FormKit buttons, making sure that your links look consistent with the rest of your form elements.

You would need to inspect a FormKit button in your application and note the Tailwind classes it uses. You might not replicate the style exactly due to the dynamic theming system, but you can identify the core classes that define its appearance (e.g., padding, font size, border radius).

Copying and pasting the generated classes is out of the question because any changes to the theme will not carry over.

Then you would need to use Vue's computed properties or methods to dynamically assign classes based on the current theme. That would assume you have a way to access or determine the current FormKit theme's styles programmatically within your Vue application.

Determine how FormKit's current theme styles, especially for buttons, can be accessed within your Vue app. That could involve importing a JavaScript object that FormKit uses for theming, accessing a global style object, or using a method provided by FormKit to get the current theme's styles. Suppose FormKit exposes its current theme's button styles through a global object or a Vue injectable. You will need to tap into this to get the styling information.

// That is an hypothetical function that gets the current theme's button styles.
// You will need to replace this with the actual way of accessing FormKit themes.
function getCurrentFormKitButtonStyles() {
  // That would return an object or string with the current styles.
  // For example: { padding: 'px-4 py-2', backgroundColor: 'bg-blue-500', }
  return formKitTheme.buttonStyles;
}

Then create a computed property:

export default {
  computed: {
    buttonClass() {
      const styles = getCurrentFormKitButtonStyles();
      // Assuming `styles` is an object, concatenate the values into a class string.
      // If it is already a string, you can return it directly.
      return Object.values(styles).join(' ');
    },
  },
}

Apply the computed classes* to router-link

<router-link :to="to" :class="buttonClass">
  <slot></slot>
</router-link>

If FormKit's theme can change dynamically at runtime, consider using a Vue reactive property to store the theme styles. Watch the source of theme changes (e.g., a theme switcher component) and update this reactive property accordingly.


The issue isn't that the theme can change dynamically, it's that the theme can be updated in the config and copying the generated classes won't work. What I need is a canonical answer about whether FormKit has any way to get at the dynamic classes for a button.

That means the core of the question now is whether FormKit offers a direct method or feature to access the dynamically generated classes that it applies to buttons, especially when considering updates in the configuration that affect theming.
Your goal would be to achieve consistency in styling a router-link to match FormKit buttons... without manually copying classes that may change when the theme configuration is updated.

As far as I know, FormKit does not provide a direct API or feature specifically designed for extracting or applying its dynamically generated classes to other elements or components outside of its own system. FormKit's documentation primarily guides on how to use its components and customize them within the scope of FormKit's own ecosystem.

Given this limitation, without direct support from FormKit to access or apply its dynamically generated classes externally, any solution will involve a degree of manual setup or maintenance to ensure stylistic alignment with FormKit's buttons.

For instance, you might consider creating a Tailwind CSS theme, assuming FormKit's dynamic classes are primarily TailwindCSS classes. But that would still involve manually copying the classes.