Contribution
Learn on how to contribute to shadcn/vue.
Introduction
Thanks for your interest in contributing to shadcn-vue.com. We're happy to have you here.
Please take a moment to review this document before submitting your first pull request. We also strongly recommend that you check for open issues and pull requests to see if someone else is working on something similar.
If you need any help, feel free to reach out to the core team on Discord.
This guide provides detailed information to help new contributors.
About this repository
This repository is a monorepo.
- We use pnpm and
workspaces
for development.
Project Structure
The GitHub repository consists of the several folders. here's a quick view.
packages - Contains source code for supporting
nuxt
as a module and thecli
to add new components.apps/www - The main folder that holds the source code for the website and every
shadcn/vue
component. This folder contains important sub-folders and is a subproject with its ownpackage.json
..vitepress - Contains the configuration and source code for
vitepress
and theshadcn/vue
website.src - Hosts the main source code for every
shadcn/vue
component or demo and their documentation on the website.__registry__ - Holds the registry file generated by
scripts/build-registry.ts
to serve components for thecli
. This folder's content is auto-generated and should not be edited manually.scripts - Contains various helper scripts, such as
build-registry.ts
, which automatically generates the__registry__
folder.content - This folder holds all the documentation for the
/docs
route. Each component has one.md
file documenting the installation and usage of the component.examples - Holds all examples not part of
/docs
, like the main page.lib/registry - The main folder hosts the source code for different styles of every component. This is likely the main folder you'll be changing.
We support two different styles for every component in shadcn/vue
:
- Default
- New York
Every component added to the repository must support both versions, including the main source code and associated demos.
When adding or modifying components, please ensure that:
- You make the changes for every style.
- You update the documentation.
- You run
pnpm build:registry
to update the registry.
Development
Start by cloning the repository:
git clone [email protected]:radix-vue/shadcn-vue.git
Install dependencies
pnpm install
Run a workspace
You can use the pnpm --filter=[WORKSPACE]
command to start the development process for a workspace or some of the shortcut command that we have setup.
Examples
- To run the
shadcn-vue.com
website:
pnpm dev
- To run the
shadcn-vue
cli package:
pnpm dev:cli
Documentation
The documentation for this project is located in the www
workspace. You can run the documentation locally by running the following command:
pnpm dev
Documentation is written using md. You can find the documentation files in the apps/www/src/content
directory.
CLI
The shadcn-vue
package is a CLI for adding components to your project. You can find the documentation for the CLI here.
Any changes to the CLI should be made in the packages/cli
directory. If you can, it would be great if you could add tests for your changes.
Testing
Tests are written using Vitest. You can run all the tests from the root of the repository.
pnpm test
Please ensure that the tests are passing when submitting a pull request. If you're adding new features, please include tests.
Commit Convention
Before you create a Pull Request, please check whether your commits comply with the commit conventions used in this repository.
When you create a commit we kindly ask you to follow the convention category(scope or module): message
in your commit message while using one of the following categories:
feat / feature
: all changes that introduce completely new code or new featuresfix
: changes that fix a bug (ideally you will additionally reference an issue if present)refactor
: any code related change that is not a fix nor a featuredocs
: changing existing or creating new documentation (i.e. README, docs for usage of a lib or cli usage)build
: all changes regarding the build of the software, changes to dependencies or the addition of new dependenciestest
: all changes regarding tests (adding new tests or changing existing ones)ci
: all changes regarding the configuration of continuous integration (i.e. github actions, ci system)chore
: all changes to the repository that do not fit into any of the above categoriese.g.
feat(components): add new prop to the avatar component
If you are interested in the detailed specification you can visit Conventional Commits.
SFC - Single File Components
Multiple components are integrated into one file in shadcn/ui
- the React version of shadcn
- while Vue only supports one component per file, hence the name Single File Component (SFC). In such cases, you need to create separate files for each component part and then export them all in an index.ts
file.
See the Accordion
source code as an example.
Wrapping Radix-Vue Components
Radix-Vue hosts many low-level UI components that are used to build reusable components. There are many cases that you need to wrap Radix-Vue
components.
Props & Events
All of the Radix-Vue
compoennts expose their prop and emit types. We need to forward any props/events that are coming from outside to the Radix-Vue
component.
To do so, we have a helper function named useForwardPropsEmits
that combines props and events that must be binded to the child radix component.
To be more clear, the function useForwardPropsEmits
takes in props and an optional emit function, and returns a computed object that combines the parsed props and emits as props.
Here's an example from Accordion
root component.
<script setup lang="ts">
import {
AccordionRoot,
type AccordionRootEmits,
type AccordionRootProps,
useForwardPropsEmits,
} from 'radix-vue'
const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
</template>
As you can see, AccordionRootEmits
and AccordionRootProps
types are imported from radix, combined with useForwardPropsEmits
and then are binded using v-bind
syntax.
CSS Classes
There are cases when we want to accept class
as a prop in our shadcn/vue
component and then combine it with a default tailwind class on our radix-vue
component via cn
utility function.
In these cases, we can not use v-bind
, because this would lead in double class binding.
Take a look at DrawerDescription.vue
.
<script lang="ts" setup>
import type { DrawerDescriptionProps } from 'vaul-vue'
import { cn } from '@/lib/utils'
import { DrawerDescription } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerDescription v-bind="delegatedProps" :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</DrawerDescription>
</template>
As you can see, we have created a computed property named delegatedProps
to remove class
from props, and only then bind the returned value to our radix component (DrawerDescription
in this case).
As for our class, we first declared it as type of HtmlHTMLAttributes['class']
and used cn
to merge tailwind classes from class
prop and our own classes.
This pattern only needs to be applied when the cn
utility is required. For instances where there are no default Tailwind classes that need to be merged with user-provided classes, this pattern is not necessary. A good example of this is the SelectValue.vue
component.
<script setup lang="ts">
import { SelectValue, type SelectValueProps } from 'radix-vue'
const props = defineProps<SelectValueProps>()
</script>
<template>
<SelectValue v-bind="props">
<slot />
</SelectValue>
</template>
Boolean Props
When you are building a wrapper for a component, in some cases you want to ignore Vue Props Boolean Casting. You can either set default value as undefined for all the boolean field, or you can use useForwardProps
composable.
Take a look at AccordionItem.vue
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<AccordionItem
v-bind="forwardedProps"
:class="cn('border-b', props.class)"
>
<slot />
</AccordionItem>
</template>
Since AccordionItemProps
type has atleast one boolean property, we need to use useForwardProps
on the entire props object.
Note that useForwardPropsEmits
use useForwardProps
under the hood.
Component as Root
Whenever your root component is a Component
Primitive from vue, it's easier to use Primitive
instead.
Let's take a look at Button.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'radix-vue'
import { type ButtonVariants, buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>
You'll need to extend PrimitiveProps
in your props to support Primitive
component. In most cases you would also need a default value for as
property.
Updating with shadcn/ui
shadcn/vue
is an unofficial, community-led Vue port of shadcn/ui
, as time goes by, they might get out of sync.
As of today, we are in sync with this commit of shadcn/ui
.
Click on the following link to check if there are newer commits that we should be synced with.
- There are no changes - If you see "There isn’t anything to compare", nothing needs to be done as we are synced with latest version.
- If there are changes, you should review thoese changes and try to apply them on
shadcn/vue
codebase and create a PR, remember to update thelatestSyncCommitTag
in this file too.
Debugging
Here are some tools and techniques that can help you debug more effectively while contributing to shadcn/vue
or developing your own projects.
Install Vue Dev Tools
To easily inspect component props, attributes, events, and more, you can leverage the Vue DevTools
extension for browsers. This extension provides a user-friendly interface for debugging Vue components and can improve your development experience.
Enable Custom Formmaters
Vue wraps values stored in a ref
in a way that, when logged, results in a nested object and requires manual inspection to access the value stored in the ref.
You can enable Custom Formatters in your browser to automate this process.