Would you be interested in a RedwoodJS UI Library?

I wouldn´t mind to contribute my templates!

2 Likes

@arimendelow , curious your opinion on @ditorodev’s proposal?

i read the proposal as shifting the focus from making a redwood ui lib to a focus of making well-paved paths to integrate existing react ui libs into redwood.

i’d personally vote for the second, but curious your thoughts ari

1 Like

@colbywhite you’re definitely picking up on a primary idea being expressed in this thread - we want a quicker, more straightforward way to get set up with UI libraries in Redwood.

@Benjamin-Lee’s idea of creating bindings on top of existing libraries sounds super interesting, and I would LOVE to see a PoC of that.

Here’s a bit of context: The Shadcn library, initially designed for NextJS, as y’all know, works with any React app. I’m envisioning something similar for a Redwood component library. While it would leverage RedwoodJS’s unique features, especially around forms, the core components would be versatile enough for use with any framework.

But here’s the catch: integrating most UI libraries with RedwoodJS isn’t straightforward yet, and honestly, I haven’t been thrilled with the existing options. Through my experience in both large-scale and greenfield React projects, I’ve come to see some patterns as less than ideal. For instance, the compound components pattern used by Shadcn and TailwindCSS’s Catalyst – while great for building – tends to be verbose and counterintuitive in practice.

What I’d additionally love to see is a theming system that’s super quick to modify, something I’ve been experimenting with recently (check out my proof of concept here: https://youtu.be/r6f7GSTwVBw).

I’ve often found myself settling for components that are “good enough” because I’d rather focus on building the app than on crafting perfect components. That’s why I’m working on a new library – one that I’d be happy to use as-is, without any tweaks, a goal I haven’t seen met by current libraries.

I’m not here to ask anyone to build a component library for me. I’m doing it because it’s something I need and believe in. But I want this to be valuable for the community too. So, I’m eager to hear from you all. What code patterns have you found effective? What API designs make complex components easier for you? @ditorodev, @Benjamin-Lee, @dennemark, I’m especially curious about your experiences.

The same thing happened with OAuth support in dbAuth - @pi0neerpat created a great PoC, Rob wrote up a nice guide, and there were some great discussions around it, but nobody had the bandwidth to build something you could just drop into your own project - so while I was implementing OAuth for myself, I approached it with the intention to build something reusable by everyone.

My aim with this library is to provide simple, yet powerful APIs for components with minimal styling – easy to use right out of the box and infinitely customizable. I’m talking about moving from complex, multi-step implementations to straightforward, clean solutions. Take a look at these two screenshots, for example:

Imagine if every time you wanted to use a Combobox, you had to do the former (not to mention that the primitive used by the former, cmdk, doesn’t allow for multiselect :roll_eyes:).

I’m determined to make this library not just a tool, but a community-driven resource. Let’s build something amazing!

1 Like

I appreciate the thorough explanation. Love the passion here.

for the sake of clarity, the framing i’m seeing is that there’s two related paths here: 1) a better react comp lib and 2) better integration of existing react comp libs into RW.

I’m curious what you see in the better-react-comp-lib path as RW-specific? one thing i could see from your vid is around forms. i could see @redwoodjs/forms being leveled up. however, i think even that package isn’t as RW-specific as it appears on the surface. it mostly contains thin (but useful) wrapper comps around react-hook-form. there are a few places with RW-specific logic, but i think the last time i discussed that package with someone from the RW team (maybe it was @Tobbe?) they mentioned that some of the RW-specific things in there might not be as useful as they once were and maybe should be deprecated.

and thus your vision of building better comps around forms could be reworded as building better comps around react-hook-form. which would still be really freaking useful, but perhaps not RW-specific.

so i’m curious where you see RW-specific benefits in path 1 coming into play?

and just for the record, if the better-comp-lib path isn’t very RW-specific, it’s still a useful effort. b/c i share a lot of your gripes around comp libs and the few comps i’ve seen from this effort already are in the i’d def use that bucket for me personally. on the surface so far tho, i’d use them in any react project, not just RW.

1 Like

oh, and on the better-integration-of-libs-in-RW path, what are people’s thoughts around how that would look like? it sounds like it’s more than just adding options to that rw setup ui <lib> command. the output of rw g cell, rw g layout, rw g page, and rw g scaffold would need to be specific to the comp lib right? anything else that would benefit from knowing the comp lib you’re using?

1 Like

Thanks, Colby! :slight_smile:

I see how the lines can blur in this context. When we talk about a component library for Redwood, it encompasses a few key aspects:

  • Designed with Redwood conventions in mind. This means:
    • Including a Storybook story for each component
    • Adhering to Redwood’s code formatting conventions
    • Ensuring seamless integration with Redwood right out of the box
  • A collaborative space where the Redwood community can actively contribute and share components.

Similarly, Shadcn’s library, though intended as part of a NextJS starter kit, is versatile enough to fit into most React projects, typically without much adjustment.

This really highlights the essence of React metaframeworks. While components from RedwoodJS or NextJS are technically compatible with any React setup, their real value lies in their thoughtful combination that significantly enhances developer experience (DX).

Instead of using Redwood, you could opt for Create React App and integrating Prisma, React Hook Form, GraphQL Yoga, etc. However, that approach would mirror Redwood already offers, and you’d lose a ton of time writing logic that isn’t specific to your app. The whole point of Redwood is the thoughtful binding all of these libraries and frameworks together, saving you the hassle of manual setup and dependency management.

Take form components, for example - as you pointed out, Redwood essentially automates the registration of form elements with React Hook Form (RHF), sparing developers the need to do it manually. It’s mostly the useRegister hook that’s a wrapper around RHF’s register.

Sure, we could use RedwoodJS form components in any project utilizing RHF, and there are countless other ways to implement forms with RHF.

But that’s the essence of Redwood: it’s a collection of these nuanced details. Individually, they might seem framework-agnostic, but when combined, they form a thoughtfully crafted metaframework.

So, yes - this component library, while broadly usable in any React project, is being developed with Redwood-specific usage in mind :slight_smile:

We’re still in the earlier stages of the project life-cycle, but I’m already considering its market positioning. Your insights align perfectly with my thoughts. Instead of marketing it as “for Redwood”, it’s more about being “by the Redwood community, for any React project, but with Redwood in mind.”

One example of this is the Combobox component. Were this intentioned to be more framework-agnostic (less opinionated), usage would look something like this:

import { useController } from 'react-hook-form'

const {
  field,
  fieldState: { error: fieldError },
} = useController({
  name,
  defaultValue: selectedValue,
  rules: { required: !props.nullable },
})

<Combobox
  {...field}
  {...otherProps}
/>

And useController would be just one way to do it, and you’d need to manually handle a lot of the form logic.

By declaring that this library is for Redwood, we’re able to include all that logic directly in the component, because Redwood has already made the opinionated decision on how forms should be built.

Anyway, I’m thrilled that you’re excited about this project! I’ll definitely keep you in the loop when it’s ready to start testing out :wink:

Agreed, it seems as though there would be a crazy amount of logistics needed to make that work. +1 on wanting thoughts from others on this.

If and when this becomes a thing, I would love to work on this as well. I have a lot of experience building component libraries across the companies I have worked for.

2 Likes

Ooh okay!! Do you have any general advice?

We’re still working on my first pass of the library - we’re at ~20 components so far, and the main thing we’re learning is exactly what Adam said in this tweet about Tailwind Catalyst - that every new component affects at least one other.

So that’s why we haven’t shared anything here yet. And once we’re done with that first pass, we’ll definitely want feedback :slight_smile:

1 Like

Hey guys! :wave: just chipping in.
Love to see the discussion. Some thoughts from me:

radix-ui + shadcn: big fan (although not using it for 1 project because it involves react-native)

scaffolding: was nice at the beginning but I’m not using it for real projects. (but the main reason is the coupling of schema, sdl and services not the UI)

current ui options: Fully agree that it’d be nice to have an option that integrates better into redwood. (with scaffold integration?? :fire:)

1 Like

I would be very desirous of and thankful for such a blog post

I’m using Vercel’s v0 product (https://v0.dev) to build out a UI

thanks!
Al;

1 Like

Are you stuck on anything specifically?

Generally, shadcn styles an input component and then does a lot of work to make it actually work with RHF.

Redwood’s input components are specific to each type of inputs, and are controlled components (meaning you don’t need to manually link each component to RHF).

There’s a few different ways you can go about using shadcn’s input designs - also see here: Forms | RedwoodJS Docs

Let me know if you have specific questions, though!

Hey Thanks!!

As soon as I can get my project running on Netlify I’ll link you over to it

It seems to be working in Dev ok - I used the manual process for the first page (built at v0.dev :+1:). a tool for doctors allowing them to select a pdf form to use when interviewing a patient | A v0.dev template - v0

I had used manual import and then asked v0 to fix problems - final version is running as generated

after I added the packages it works ok so far

Here are the components

import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "src/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline:
          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }
import * as React from "react"

import { cn } from "src/lib/utils"

const Card = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "rounded-xl border bg-card text-card-foreground shadow",
      className
    )}
    {...props}
  />
))
Card.displayName = "Card"

const CardHeader = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex flex-col space-y-1.5 p-6", className)}
    {...props}
  />
))
CardHeader.displayName = "CardHeader"

const CardTitle = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h3
    ref={ref}
    className={cn("font-semibold leading-none tracking-tight", className)}
    {...props}
  />
))
CardTitle.displayName = "CardTitle"

const CardDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <p
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
CardDescription.displayName = "CardDescription"

const CardContent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"

const CardFooter = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex items-center p-6 pt-0", className)}
    {...props}
  />
))
CardFooter.displayName = "CardFooter"

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
import * as React from "react"

import {
  CaretSortIcon,
  CheckIcon,
  ChevronDownIcon,
  ChevronUpIcon,
} from "@radix-ui/react-icons"

import * as SelectPrimitive from "@radix-ui/react-select"

import { cn } from "src/lib/utils"

const Select = SelectPrimitive.Root

const SelectGroup = SelectPrimitive.Group

const SelectValue = SelectPrimitive.Value

const SelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Trigger
    ref={ref}
    className={cn(
      "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
      className
    )}
    {...props}
  >
    {children}
    <SelectPrimitive.Icon asChild>
      <CaretSortIcon className="h-4 w-4 opacity-50" />
    </SelectPrimitive.Icon>
  </SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName

const SelectScrollUpButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollUpButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronUpIcon />
  </SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName

const SelectScrollDownButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollDownButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronDownIcon />
  </SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
  SelectPrimitive.ScrollDownButton.displayName

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className={cn(
        "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        position === "popper" &&
          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
        className
      )}
      position={position}
      {...props}
    >
      <SelectScrollUpButton />
      <SelectPrimitive.Viewport
        className={cn(
          "p-1",
          position === "popper" &&
            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
        )}
      >
        {children}
      </SelectPrimitive.Viewport>
      <SelectScrollDownButton />
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName

const SelectLabel = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Label
    ref={ref}
    className={cn("px-2 py-1.5 text-sm font-semibold", className)}
    {...props}
  />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Item
    ref={ref}
    className={cn(
      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      className
    )}
    {...props}
  >
    <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
      <SelectPrimitive.ItemIndicator>
        <CheckIcon className="h-4 w-4" />
      </SelectPrimitive.ItemIndicator>
    </span>
    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
  </SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName

const SelectSeparator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
    {...props}
  />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName

export {
  Select,
  SelectGroup,
  SelectValue,
  SelectTrigger,
  SelectContent,
  SelectLabel,
  SelectItem,
  SelectSeparator,
  SelectScrollUpButton,
  SelectScrollDownButton,
}

and the page

import { Link, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'

/**
 * v0 by Vercel.
 * @see https://v0.dev/t/7GJwuQ021mN
 */
import { Button } from 'src/components/Button';
import { CardTitle, CardDescription, CardHeader, CardContent, Card, CardFooter } from 'src/components/Card';
import { SelectValue, SelectTrigger, SelectItem, SelectContent, Select } from 'src/components/Select';

const SelectForInterviewPage = () => {
  return (
    <div className="w-full h-screen bg-gray-100 dark:bg-gray-900 z-20">
      <header className="container mx-auto px-4 md:px-6 lg:px-8 py-10 flex items-center justify-between z-20">
        <h1 className="text-4xl font-bold text-gray-800 dark:text-gray-200 z-20">Product Title</h1>
        <Button className="bg-white dark:bg-gray-800 rounded-full p-2 z-20">
          <img alt="User Icon" className="w-8 h-8 z-20" src="/placeholder.svg" />
        </Button>
      </header>
      <div className="container mx-auto px-4 md:px-6 lg:px-8 py-10 z-20">
        <div className="grid grid-cols-1 md:grid-cols-3 gap-6 z-20">
          <div className="md:col-span-1 flex flex-col z-20">
            <Card className="h-1/2 bg-white dark:bg-gray-800 z-20">
              <CardHeader className="z-20">
                <CardTitle className="z-20">Select a Category</CardTitle>
                <CardDescription className="z-20">Choose a category to narrow down the form selection.</CardDescription>
              </CardHeader>
              <CardContent className="z-20">
                <Select className="z-20">
                  <SelectTrigger className="w-full z-20">
                    <SelectValue className="z-20" placeholder="Select a category" />
                  </SelectTrigger>
                  <SelectContent className="bg-white dark:bg-gray-800">
                    <SelectItem value="category1">Intake</SelectItem>
                    <SelectItem value="category2">Medical History</SelectItem>
                    <SelectItem value="category3">Consent</SelectItem>
                    <SelectItem value="category4">Assessment</SelectItem>
                  </SelectContent>
                </Select>
              </CardContent>
            </Card>
            <Card className="h-1/2 mt-6 bg-white dark:bg-gray-800 z-20">
              <CardHeader className="z-20">
                <CardTitle className="z-20">Select a Form</CardTitle>
                <CardDescription className="z-20">
                  Choose a form to use during the patient interview process.
                </CardDescription>
              </CardHeader>
              <CardContent className="z-20">
                <Select className="z-20">
                  <SelectTrigger className="w-full z-20">
                    <SelectValue className="z-20" placeholder="Select a form" />
                  </SelectTrigger>
                  <SelectContent className="bg-white dark:bg-gray-800">
                    <SelectItem value="form1">Patient Intake Form</SelectItem>
                    <SelectItem value="form2">Medical History Form</SelectItem>
                    <SelectItem value="form3">Consent Form</SelectItem>
                    <SelectItem value="form4">Health Assessment Form</SelectItem>
                  </SelectContent>
                </Select>
              </CardContent>
            </Card>
          </div>
          <div className="md:col-span-2 z-20">
            <Card className="h-full flex flex-col bg-white dark:bg-gray-800 z-20">
              <CardHeader className="z-20">
                <CardTitle className="z-20">Form Preview</CardTitle>
                <CardDescription className="z-20">
                  Preview the selected form before using it during the patient interview.
                </CardDescription>
              </CardHeader>
              <CardContent className="flex-grow z-20">
                <div className="w-full h-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md overflow-auto z-20">
                  <div className="w-full h-full z-20" />
                </div>
              </CardContent>
              <CardFooter className="flex justify-end gap-4 p-4 z-20">
                <Button className="bg-green-500 text-white z-20">Download</Button>
                <Button className="bg-blue-500 text-white z-20">Print</Button>
                <Button className="bg-red-500 text-white z-20">Delete</Button>
              </CardFooter>
            </Card>
          </div>
        </div>
      </div>
    </div>
  )
 }

export default SelectForInterviewPage

ok, so that other issue is taken care of…

I’m going to be using v0 for sure

how does your usage track with mine ?

I must code - so manual processes will be replaced

Cheers
Al;

What was the issue?

I don’t use V0 :slight_smile: I find that it’s generally faster for me to write code from scratch than to generate code and then massage it to my liking. I do use Github Copilot quite a bit, though.

@arimendelow Curious if this was shared, maybe somewhere else?

Not just yet! It’s still very much a WIP. Had to take a pause for a few months to work on some other things.

Stay tuned :slight_smile:

And if there’s anything specific you’re interested in, do let us know!

Absolutely! A RedwoodJS UI Library would be a game-changer, making development even more seamless and efficient.

1 Like

Thanks for providing this solution... It’s a valuable contribution to problem-solving.