Introduction
A UI component generation solution that starts from the essence of interaction
What is this
This project is named Prototype UI, a component generation solution based on Prototype.
Currently in its early stages, it appears to be a Web Component or pure JavaScript version of Radix UI. Additionally, the documentation site and component styles are inspired by shadcn/ui.
Project Status
🚧 This project is currently in early development stage.
What's Available
- Basic Web Component implementation
- Documentation site (you're looking at it)
- Core prototype system
What's Coming
- CLI tool for component installation
- More component implementations
- More theme options
- Framework adapters (React, Vue, etc.)
Why we do this
The design philosophy of Prototype UI is Adapter(Prototype) => Component
We have used too many technologies to build interfaces, such as React, Vue, Svelte, Solid for the web, native Flutter, Qt, etc. Every time we write UI, we always start with components like Button. We've written Button too many times, yet their interactive essence remains the same.
Prototype UI hopes to find a balance between interaction and development, seeking a solution that is adaptable enough for all platforms while starting from the essence of interaction.
When the next operating system emerges, we won't need to develop a new set of UI libraries for each design language; when the next design language appears, we won't need to struggle to adapt it to all technical solutions.
Perhaps when the next interaction system appears, we can have a more reasonable starting point, rather than rewriting Button over and over again.
Get Involved
We're in the early stages of development and would love your input!
How to Contribute
- Star the project on GitHub
- Open issues to discuss ideas or report bugs
- Submit pull requests for improvements
- Share your thoughts in discussions
Development Roadmap
- Phase One: Web Platform
- Complete core prototype system
- Implement basic components
- Build CLI tool
- Improve documentation
- Phase Two: Cross Platform
- Add framework adapters
- Support more platforms
- Expand design language themes
Current Focus
- Stabilizing the core prototype system
- Implementing basic components
- Building the documentation site
On This Page
Quick Start
To use it, to contribute, whatever the purpose, we need to get it running first, which is fortunately very simple
Installation and Running
Since there is no CLI tool to help you install yet, we recommend cloning the project source code and running it locally
git clone https://github.com/guangliang2019/Prototype-UI.git
Make sure you have Node.js 18 or higher installed, then run the following commands in the directory containing package.json
npm install
npm run dev
If successful, you will be able to see the Prototype UI documentation site at http://localhost:5173/
Preparation Before Use
If you want to use the components provided by Prototype UI, such as Web Component style Headless Button
or Shadcn theme Button
You can directly copy the prototype
and shadcn
directories from the project's src/components
directory to your project. Additionally, you need to copy the src/core
directory to your project
src/core/adapters
includes various types of adapters. It is recommended to delete adapters that your project does not use. For example, if you only use Web Component style components, then you only need to keep the src/core/adapters/web/@web-component
directory
If you don't delete unnecessary adapters, it may cause your project size to increase (adapters have no dependencies between them, it's recommended to decide which to keep based on your project)
In the future, we will provide a CLI tool to help you write components directly into your project, but for now, you'll have to handle it manually
Prototype and Component
The design philosophy of Prototype UI is Adapter(Prototype) => Component
, so you'll see code like this in the src/components
directory
export const PrototypeButtonPrototype = definePrototype({
name: 'prototype-button',
setup: asButton,
});
export const PrototypeButton = WebComponentAdapter(PrototypeButtonPrototype);
In the above example, we define a prototype PrototypeButtonPrototype
, then use WebComponentAdapter
to adapt it into a Web Component PrototypeButton
In the setup
function of the prototype PrototypeButtonPrototype
, we use the asButton
function, which adapts the prototype to button behavior
We will explain the setup
function and asButton
function in detail later
The final output PrototypeButton
component has APIs and behavior consistent with native Web Components. It can be inherited, extended, and any Web Component features can be used
The components in the current documentation site are developed based on prototypes-generated Web Components, rather than using the prototype and adapter pattern from start to finish, because we need to ensure that the output components are truly consistent with the developer experience of native components
setup Function
setup
is the exact description of prototype behavior, representing the common logic part of implementing the same interactive component across any platform
The asButton
used earlier is one of the default setup behaviors provided by Prototype UI, using them is not mandatory
The setup
function has one parameter, usually called p
, which is the "remote control" of the current prototype. What you need to do is use it in the setup
function to describe the behavior of the current prototype
For example, a custom button that prints "Hello Prototype UI" to the console when clicked
const MyButtonPrototype = definePrototype({
name: 'my-button',
setup: (p) => {
p.event.on('click', () => {
p.debug.log('Hello Prototype UI')
})
},
});
In the above code, we use p.event.on
to listen for click events and p.debug.log
to print logs
p
has many capabilities, which we will gradually explore
More About p's Capabilities
p
's functionality is divided into several major parts. This section might contain a lot of information. If it's difficult to understand at first, you can skip it and come back to consult when needed
p.event
Used to listen for interaction events, the only way for components to interact with end users
p.debug
Used for printing logs, recording performance, enabling debugging features, etc. This feature is very useful during development
p.props
Used to set, modify, and listen to component property parameters, such as button style types (primary button, secondary button, border button, ghost button, danger button)
p.state
Used to set and listen to component states. Here, states refer to states similar to animation state machines, such as button disabled state, active state, focus state, etc.
p.context
Used to provide, listen to, and obtain component context. Context refers to data shared between different parts of a component
p.lifecycle
Used to execute custom logic at specified component lifecycle stages
p.view
's functionality is divided into two categories, mainly related to rendering scheduling and elements
- Rendering scheduling related, such as requesting view updates, forcing view updates
- Element related, such as obtaining runtime rendering elements, comparing element positions, obtaining specific element parameters, etc.
Let's take an example, such as a Tabs
component, which consists of TabsTrigger
and TabsContent
two parts
The logic is that when TabsTrigger
is clicked, add the data-selected
attribute to the TabsContent
that carries the same value as TabsTrigger
, making it convenient for subsequent custom styling
const MyTabsContext = createContext<{value: string; changeTab: (value: string) => void}>('my-tabs')
const MyTabsPrototype = definePrototype({
name: 'my-tabs',
setup: (p) => {
p.props.define({
defaultValue: ""
})
p.context.provide(MyTabsContext, (updateContext) => {
const props = p.props.get()
return {
value: props.defaultValue,
changeTab: (value: string) => {
updateContext({
value
})
}
}
})
},
});
const MyTabsTriggerPrototype = definePrototype({
name: 'my-tabs-trigger',
setup: (p) => {
p.props.define({
value: ""
})
const selected = p.state.define(false, 'data-selected')
p.context.watch(MyTabsContext, (context, changedKeys) => {
if (changedKeys.includes('value')) {
selected.set(context.value === p.props.get().value)
}
})
p.event.on('click', () => {
const context = p.context.get(MyTabsContext)
const props = p.props.get()
context.changeTab(props.value)
})
}
});
const MyTabsContentPrototype = definePrototype({
name: 'my-tabs-content',
setup: (p) => {
p.props.define({
value: ""
})
const selected = p.state.define(false, 'data-selected')
p.context.watch(MyTabsContext, (context, changedKeys) => {
if (changedKeys.includes('value')) {
selected.set(context.value === p.props.get().value)
}
})
}
});
Return Value of setup
In the previous examples, we mainly demonstrated the usage of p
in the setup
function body. Actually, the return value of the setup
function is also very important
The return value of the setup
function is an object with 4 components, all of which are optional, with render
and expose
being more commonly used
render
The component's rendering template, default is to use the current component as a slot. Using custom rendering functions can control the rendering structure
expose
APIs exposed by the current component to the outside, these APIs can be easily used externally
states
If the currentsetup
function is used as a hook function, which internal states can be controlled by external components
actions
If the currentsetup
function is used as a hook function, which internal states can be controlled by external components
const MyTabsContext = createContext<{value: string; changeTab: (value: string) => void}>('my-tabs')
const MyTabsPrototype = definePrototype({
name: 'my-tabs',
setup: (p) => {
p.props.define({
defaultValue: ""
})
p.context.provide(MyTabsContext, (updateContext) => {
const props = p.props.get()
return {
value: props.defaultValue,
changeTab: (value: string) => {
updateContext({
value
})
}
}
})
return {
exposes: {
changeTab: (value: string) => {
p.context.get(MyTabsContext).changeTab(value)
}
}
}
},
});
After adding exposes, we can use the changeTab
function to control the component's internal state from outside the component
Taking Web Component as an example, you can directly get the Tabs
root component's DOM element, then directly call the changeTab
method to control Tabs switching
Like this:
const tabs = document.querySelector('my-tabs')
tabs.changeTab('tab-1')
Different Adapters
handle exposes
differently, but they all try to keep the usage experience close to the target technical solution
On This Page
Button
Displays a button or a component that looks like a button.
<shadcn-button variant="primary">Button</shadcn-button>
Examples
Primary
<shadcn-button variant="primary">Primary</shadcn-button>
Secondary
<shadcn-button>Secondary</shadcn-button>
Outline
<shadcn-button variant="outline">Outline</shadcn-button>
Ghost
<shadcn-button variant="ghost">Ghost</shadcn-button>
Link
<shadcn-button variant="link">Link</shadcn-button>
Destructive
<shadcn-button variant="destructive">Destructive</shadcn-button>
On This Page
Card
Card are used to organize content on the page.
Create project
Deploy your new project in one-click
Code component is in development
On This Page
Tabs
Tabs are used to organize content on the page.
Account
Make changes to your account here. Click save when you're done.
Password
Change your password here. After saving, you'll be logged out.
Code component is in development
On This Page
Select
Displays a select or a component that looks like a select.
Code component is in development
On This Page
Input
Displays a select or a component that looks like a select.
Code component is in development
On This Page
Scroll Area
Scroll Area
Code component is in development
On This Page
Switch
A switch component.
<shadcn-switch></shadcn-switch>
On This Page
Transition
Transition is used to open or hide content.
<prototype-transition id="transition" class="opacity-[0.95] size-[6.25rem] block rounded-xl bg-primary shadow-lg transition duration-[400ms] data-[closed]:scale-50 data-[closed]:rotate-[-120deg] data-[closed]:opacity-0 data-[leave]:duration-[200ms] data-[leave]:ease-in-out data-[leave]:data-[closed]:scale-95 data-[leave]:data-[closed]:rotate-[0deg]"></prototype-transition><shadcn-button id="button" class="mt-10">Toggle Transition</shadcn-button><script> const button = document.getElementById('button'); const transition = document.getElementById('transition'); button?.onClick = () => { transition.open = !transition.open; }</script>
Overlay test
<shadcn-button tabindex="0" class="select-none whitespace-nowrap inline-flex items-center justify-center gap-2 rounded-md h-9 px-4 py-2 cursor-pointer text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring shadow-sm bg-secondary text-secondary-foreground hover:bg-secondary/80" data-active=""> Toggle Overlay</shadcn-button><prototype-overlay style="position: relative; width: 0px; height: 0px;"></prototype-overlay>
Motion Scroll Test
On This Page
Select
Displays a list of options for the user to pick from—triggered by a button.
Code component is in development
On This Page
Scroll Area
Scroll Area
Code component is in development
On This Page
Radio Group
Displays a list of options for the user to pick from—triggered by a button.
Code component is in development
On This Page
Resizable
Accessible resizable panel groups and layouts with keyboard support.
Code component is in development
On This Page
Button
Describe Button
Code component is in development
On This Page
TestTab
Describe TestTab
Code component is in development
On This Page
TestButton
Describe TestButton
Code component is in development
On This Page
Switch 开关
开关组件允许用户在两种状态之间切换
On This Page
Checkbox
复选框组件
On This Page
Check out some examples
Dashboard, cards, authentication. Some examples built using the components.
Use this as a guide to build your own.