Fix linting config and package fields
- Replace .eslintrc.cjs with eslint.config.mjs (ESLint 9 flat config)
using direct eslint-plugin-solid + @typescript-eslint/parser approach
- Add @typescript-eslint/parser to root devDependencies
- Add main/module/types top-level fields to packages/core/package.json
- Add resolve.conditions to packages/core/vite.config.ts
- Create packages/core/tsconfig.test.json for test type-checking
- Remove empty paths:{} from packages/core/tsconfig.json
This commit is contained in:
parent
405ce15933
commit
db906fd85a
@ -1,19 +0,0 @@
|
|||||||
/** @type {import('eslint').Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
plugins: ["solid"],
|
|
||||||
extends: ["plugin:solid/typescript"],
|
|
||||||
rules: {
|
|
||||||
// Only Solid-specific rules — everything else handled by Biome
|
|
||||||
"solid/reactivity": "error",
|
|
||||||
"solid/no-destructure": "error",
|
|
||||||
"solid/prefer-for": "warn",
|
|
||||||
"solid/no-react-deps": "error",
|
|
||||||
"solid/no-react-specific-props": "error",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
2154
.firecrawl/apis-for-ai.md
Normal file
2154
.firecrawl/apis-for-ai.md
Normal file
File diff suppressed because it is too large
Load Diff
455
.firecrawl/beyond-bespoke-ui.md
Normal file
455
.firecrawl/beyond-bespoke-ui.md
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
Authors
|
||||||
|
|
||||||
|
- NameDaniel CressTwitter
|
||||||
|
|
||||||
|
# Beyond Bespoke: How AI Turns Component Libraries Into Adaptive Systems
|
||||||
|
|
||||||
|
Every time we build a new feature, we follow the same ritual: design mockups, build custom components, wire up state management, test variations, deploy. Then the requirements change slightly, and we do it all again. A dashboard for managers needs different cards than one for employees. A mobile form needs different layouts than desktop. A beginner's view needs simpler options than an expert's.
|
||||||
|
|
||||||
|
We've gotten really good at building bespoke experiences. Maybe too good. We've optimized the process of creating unique interfaces for every context, every user type, every edge case. But what if we're solving the wrong problem?
|
||||||
|
|
||||||
|
What if instead of building better tools for creating variations, we built systems that generate variations on demand? What if our component libraries could adapt themselves based on context, using the same primitives we already have?
|
||||||
|
|
||||||
|
This isn't about replacing Nuxt UI or Shadcn or Radix. It's about teaching them to compose themselves.
|
||||||
|
|
||||||
|
## What We Built at Bambee
|
||||||
|
|
||||||
|
At Bambee, we built a system that generates contextual UI based on vast amounts of workplace data—performance metrics, compliance requirements, employee sentiment, turnover patterns, organizational health indicators. The challenge wasn't just showing data; it was surfacing the right insights with the right actions at the right time.
|
||||||
|
|
||||||
|
A manager dealing with high turnover needs different recommendations than one managing a stable team. An employee in their first month needs different guidance than a three-year veteran. A compliance alert for California employment law requires different visualizations than one for federal OSHA requirements.
|
||||||
|
|
||||||
|
We started down the familiar path: custom components for each scenario. But the combinatorial explosion quickly became clear. Dozens of card types. Countless variations of forms. Complex conditional logic everywhere.
|
||||||
|
|
||||||
|
So we tried something different.
|
||||||
|
|
||||||
|
### Dynamic Cards and Notices
|
||||||
|
|
||||||
|
The system analyzes company data and generates recommendations—we call them solutions and notices. Each one has different severity levels, different actions users can take, different visualizations to make the data clear.
|
||||||
|
|
||||||
|
One user might see a compliance alert with a bar chart showing policy gaps across departments. Another sees a performance insight with a timeline visualization of team productivity trends. A third sees a recognition opportunity with a simple progress indicator.
|
||||||
|
|
||||||
|
Here's the key: we're not building separate components for each type. We're using the same card component, the same underlying UI primitives from our component library. What changes is the structured data that drives them.
|
||||||
|
|
||||||
|
The system generates a schema-compliant payload that describes what to show, how to show it, and what actions should be available. The frontend trusts that structure and renders accordingly.
|
||||||
|
|
||||||
|
### Dynamic Wizards
|
||||||
|
|
||||||
|
The second use case was even more interesting: multi-step wizards that adapt to context.
|
||||||
|
|
||||||
|
The system detects information gaps—missing data that would improve recommendations. Based on what's missing and how critical it is, it generates a wizard to collect that information.
|
||||||
|
|
||||||
|
Sometimes it's a simple two-step survey: "What's your management philosophy? How large is your team?" Other times it's a comprehensive five-step wizard with conditional logic: if you answer yes to one question, you see follow-up questions; if you answer no, you skip to the next section.
|
||||||
|
|
||||||
|
The question types change dynamically. Sliders for rating scales. Checkbox groups for multiple selections. Matrices for comparing options across criteria. Date pickers for timelines. All generated from schemas, all rendered by the same form components.
|
||||||
|
|
||||||
|
We're not pre-building every possible wizard variation. We're defining the contract—what a wizard can contain, what question types are valid, how steps can be arranged—and letting the system compose variations on the fly.
|
||||||
|
|
||||||
|
## The Core Principles
|
||||||
|
|
||||||
|
Building this taught us some things about how to structure systems for dynamic UI generation. These aren't prescriptive rules, just patterns that worked for us.
|
||||||
|
|
||||||
|
### Schema as Contract
|
||||||
|
|
||||||
|
In traditional development, you design an interface, build the component, then wire it to data. The component defines what's possible.
|
||||||
|
|
||||||
|
In schema-driven development, you define the contract first. The schema is the source of truth. It describes what shapes of data are valid, what fields are required, what values are acceptable.
|
||||||
|
|
||||||
|
The backend generates data conforming to that schema. The frontend trusts the structure and renders accordingly. Neither side makes assumptions beyond what the schema guarantees.
|
||||||
|
|
||||||
|
This inverts the usual relationship. Instead of data conforming to components, components adapt to data (within the bounds of the schema).
|
||||||
|
|
||||||
|
A simple example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Schema defines possibilities
|
||||||
|
ActionCardSchema = {
|
||||||
|
type: 'ACTION_CARD',
|
||||||
|
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL',
|
||||||
|
action: {
|
||||||
|
type: string,
|
||||||
|
label: string,
|
||||||
|
handler: { route: string, params: object }
|
||||||
|
},
|
||||||
|
visualization?: {
|
||||||
|
type: 'BAR' | 'LINE' | 'PIE',
|
||||||
|
data: object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend generates instance based on context
|
||||||
|
const generatedCard = analyzeContext(userData)
|
||||||
|
|
||||||
|
// Frontend renders using component library primitives
|
||||||
|
<CardSlot data={generatedCard} />
|
||||||
|
```
|
||||||
|
|
||||||
|
The schema is doing a lot of work here. It's defining not just data types, but the vocabulary of what interfaces can express. Add a new visualization type to the schema, teach the frontend to render it, and suddenly all generated cards can use it.
|
||||||
|
|
||||||
|
### Type Safety Through Validation
|
||||||
|
|
||||||
|
The critical enabler for this approach is runtime validation. For us, that means Zod, but the principle applies to any schema validation library.
|
||||||
|
|
||||||
|
Here's why it matters: AI generates JSON. That JSON must match the exact structure your frontend expects. If it doesn't, you get runtime errors, broken UI, frustrated users.
|
||||||
|
|
||||||
|
With runtime validation, you create a feedback loop. AI generates output. You validate it immediately against your schema. If it fails validation, you send the errors back to the AI and ask it to regenerate.
|
||||||
|
|
||||||
|
The pattern looks like:
|
||||||
|
|
||||||
|
1. AI generates structured output based on context
|
||||||
|
2. Validate against schema immediately
|
||||||
|
3. If invalid → capture specific validation errors
|
||||||
|
4. Send those errors back to AI with original context
|
||||||
|
5. AI regenerates, accounting for what went wrong
|
||||||
|
6. Retry with exponential backoff (2-3 attempts max)
|
||||||
|
7. If all retries fail → fallback to safe default
|
||||||
|
|
||||||
|
This creates remarkably reliable output. The AI learns your schema requirements through the error messages. After a few iterations of improving prompts and tightening schemas, validation failures become rare.
|
||||||
|
|
||||||
|
The validation itself is straightforward:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const validated = CardSchema.parse(aiOutput)
|
||||||
|
return validated
|
||||||
|
} catch (error) {
|
||||||
|
await retryWithRefinement(aiOutput, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
But the implications are profound. Your frontend never sees invalid data. Type safety is enforced at runtime. Breaking changes to schemas are caught immediately, not in production.
|
||||||
|
|
||||||
|
### Slots Over Specifics
|
||||||
|
|
||||||
|
We stopped building `<ComplianceCard>`, `<PerformanceCard>`, `<OnboardingCard>`. Instead, we built `<ActionSlot>` that accepts structured data and routes to appropriate presentation.
|
||||||
|
|
||||||
|
The slot examines the schema, determines which component primitives to use, and composes the final UI.
|
||||||
|
|
||||||
|
A compliance alert might render as: red badge with alert icon, bar chart visualization, "Review Policy" button that routes to policy creation. A performance insight might render as: blue badge with trend icon, line chart visualization, "View Details" button that opens a details modal.
|
||||||
|
|
||||||
|
Same slot. Same underlying Button, Card, Badge, and Chart components from our component library. Different compositions based on the schema data.
|
||||||
|
|
||||||
|
This is more than just abstraction. It's a different mental model. You're not building components for specific use cases. You're building a rendering engine that interprets schemas and composes UI from primitives.
|
||||||
|
|
||||||
|
The power comes from the mapping layer. It reads the schema and makes decisions:
|
||||||
|
|
||||||
|
- What badge color and icon represent this severity level?
|
||||||
|
- Which chart component matches this visualization type?
|
||||||
|
- What button variant and text match this action type?
|
||||||
|
- How should these elements be arranged for this context?
|
||||||
|
|
||||||
|
As you add more schema types, the mapping layer grows. But the underlying components stay the same.
|
||||||
|
|
||||||
|
### Visualization as Configuration
|
||||||
|
|
||||||
|
Instead of building separate chart components for every context, we treat visualizations as configuration.
|
||||||
|
|
||||||
|
The backend sends structured data describing what to visualize and how:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
visualizationType: 'BAR',
|
||||||
|
data: {
|
||||||
|
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
|
||||||
|
values: [23, 45, 67, 89]
|
||||||
|
},
|
||||||
|
theme: 'minimal',
|
||||||
|
options: { showLegend: false }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The frontend has a factory function. It looks at the visualization type and says, "BAR chart? Render the BarChart component from our library with these props."
|
||||||
|
|
||||||
|
This means adding a new visualization type is a small change. Add the type to your schema, teach the factory to handle it, and now every part of your system that generates visualizations can use it.
|
||||||
|
|
||||||
|
We extended this with a vector database of illustrations. The system can describe the visual context it needs—"performance improvement scenario with upward trend"—then query embeddings to find the closest matching illustration from our library. No manual asset selection. Just semantic matching between generated context and available visuals.
|
||||||
|
|
||||||
|
### Dynamic Wizards from Schemas
|
||||||
|
|
||||||
|
Multi-step forms become declarative data structures.
|
||||||
|
|
||||||
|
A wizard schema defines:
|
||||||
|
|
||||||
|
- How many steps
|
||||||
|
- What questions appear in each step
|
||||||
|
- Question types and validation rules
|
||||||
|
- Conditional display logic
|
||||||
|
- Progress indicators and navigation
|
||||||
|
|
||||||
|
The frontend loops through the schema and renders appropriate input components. A question with type `'SLIDER'` renders your library's slider component. A question with type `'CHECKBOX'` renders a checkbox group.
|
||||||
|
|
||||||
|
The power is in the conditional logic. Questions can show or hide based on previous answers. Entire steps can be skipped based on user context. Validation rules can reference other questions.
|
||||||
|
|
||||||
|
All described in the schema. All rendered by generic form components.
|
||||||
|
|
||||||
|
Adding a new question type means updating the schema to include it and teaching one component to render it. Not rebuilding every wizard that might use it.
|
||||||
|
|
||||||
|
This is where it clicked for me: we weren't building forms anymore. We were building a form generation engine.
|
||||||
|
|
||||||
|
## The Future Vision
|
||||||
|
|
||||||
|
This is where it gets interesting. What we built at Bambee is a proof of concept. It works in production, handles real complexity, serves real users. But it's just scratching the surface of what's possible.
|
||||||
|
|
||||||
|
Let me paint some pictures of where this could go. To illustrate these concepts without diving into proprietary specifics, I'll use a hypothetical recipe and meal planning platform as a concrete example—but these patterns apply across any domain with similar variability.
|
||||||
|
|
||||||
|
### Pages That Generate Themselves
|
||||||
|
|
||||||
|
Imagine you're building a recipe and meal planning platform. A user opens the app and says, "I want to plan meals for the week."
|
||||||
|
|
||||||
|
The system analyzes their context:
|
||||||
|
|
||||||
|
- Family size and ages
|
||||||
|
- Dietary restrictions and preferences
|
||||||
|
- Available cooking time
|
||||||
|
- Current pantry inventory
|
||||||
|
- Cooking skill level
|
||||||
|
- Budget constraints
|
||||||
|
- Past meal preferences
|
||||||
|
|
||||||
|
From this, it generates a complete page schema. Not a page that exists in your codebase. A page composed on the fly:
|
||||||
|
|
||||||
|
A three-column dashboard layout. Left column shows a weekly calendar with meal slots, each slot showing prep time and ingredient overlap with other meals. Center column shows a shopping list organized by grocery store section, with cost estimates and substitution suggestions. Right column shows a nutrition summary chart aggregating the week's macros, plus a comparison to their goals.
|
||||||
|
|
||||||
|
At the bottom, an action row with context-aware buttons: export meal plan to calendar, send shopping list to grocery app, adjust for budget, regenerate for more variety.
|
||||||
|
|
||||||
|
The frontend receives this schema and composes it using your component library's Grid, Card, Calendar, List, Chart, and Button components. The page never existed before this moment. It was assembled because this specific user, in this specific context, needed this specific combination of features.
|
||||||
|
|
||||||
|
That's the radical shift: from pages as files in a repository to pages as generated compositions. The repository contains the components and the schemas. The combinations emerge from context.
|
||||||
|
|
||||||
|
### Context-Aware Form Adaptation
|
||||||
|
|
||||||
|
Same underlying data, completely different form based on who's using it.
|
||||||
|
|
||||||
|
A beginner user gets a simplified recipe entry form: basic fields, lots of help text, suggested defaults, links to video tutorials explaining cooking terms. Single-column layout, large touch targets, progress saved after every field.
|
||||||
|
|
||||||
|
An advanced user gets the compact version: all fields visible, technical terminology, advanced options like ingredient ratios and technique variations, keyboard shortcuts for quick entry. Multi-column layout, minimal explanatory text, batch editing capabilities.
|
||||||
|
|
||||||
|
The system decides which variation to show based on user behavior analysis. Not A/B testing. Not user segments. Individual adaptation.
|
||||||
|
|
||||||
|
And it goes deeper. Forms that adapt mid-flow based on answers. User selects "dietary restriction: vegan" → the form immediately hides all questions about meat preparation, adds questions about B12 supplementation, adjusts the nutrition target ranges, suggests vegan protein sources in the ingredient picker.
|
||||||
|
|
||||||
|
The form is responding to context in real-time, reshaping itself to show what's relevant and hide what isn't.
|
||||||
|
|
||||||
|
### Adaptive Complexity
|
||||||
|
|
||||||
|
Interfaces that scale complexity based on user sophistication, not just hiding advanced features behind a settings toggle.
|
||||||
|
|
||||||
|
A recipe platform might show the same dish completely differently based on skill level. Beginners see: "Sauté the onions until soft" with a photo showing what "soft" looks like and a link to a basic sautéing video. Intermediate cooks see: "Sauté onions in butter over medium heat until translucent, 5-7 minutes" with suggested pan types and heat settings. Advanced cooks see: "Sweat onions in clarified butter, 82°C, until cell walls break down but no Maillard reaction occurs—monitor for steam release, not browning" with technique alternatives like using a immersion circulator for precise temperature control.
|
||||||
|
|
||||||
|
The ingredient list adapts too. Beginners see standard grocery store items. Intermediate cooks see preferred brands and substitution options. Advanced users see specific varieties ("Vidalia onions for sweetness, or shallots for depth"), quality indicators, and even molecular composition notes for technique-critical ingredients.
|
||||||
|
|
||||||
|
Same recipe. Same underlying data. But the interface reveals layers of technical sophistication progressively, matching what each user can handle and wants to see.
|
||||||
|
|
||||||
|
The system watches behavior. User consistently completes advanced recipes without issues? Start showing more complexity. User struggles with intermediate recipes? Pull back to basics.
|
||||||
|
|
||||||
|
This isn't just hiding fields or showing tooltips. It's fundamentally different interfaces generated for different capability levels, all from the same underlying schemas.
|
||||||
|
|
||||||
|
### Cross-Domain Schema Standards
|
||||||
|
|
||||||
|
Here's where my mind goes to interesting places.
|
||||||
|
|
||||||
|
What if schema patterns became standardized across domains?
|
||||||
|
|
||||||
|
An `ACTION_CARD` schema could power meal suggestions in a recipe app, workout recommendations in a fitness app, budget alerts in a finance app, treatment plan updates in a healthcare app. Different domains, same structure: context analysis → recommended action → visualization → user choice.
|
||||||
|
|
||||||
|
A `WIZARD` schema could power dietary preference surveys in a recipe app, goal-setting wizards in a fitness app, budget creation flows in a finance app, symptom checkers in a healthcare app. Same multi-step structure, same question types, same conditional logic patterns.
|
||||||
|
|
||||||
|
Your component library becomes a universal renderer for structured intents. You build the primitives once—cards, forms, charts, buttons—and they work across every domain that speaks the schema language.
|
||||||
|
|
||||||
|
This is bigger than code reuse. It's conceptual reuse. The patterns for how to structure adaptive UI become portable knowledge, not locked into specific implementations.
|
||||||
|
|
||||||
|
Imagine open schema standards for common UI patterns. Like how we have standard HTTP methods or standard database query languages, we could have standard schemas for "action recommendation with visualization" or "multi-step data collection with conditional logic."
|
||||||
|
|
||||||
|
Build a great implementation once, use it everywhere.
|
||||||
|
|
||||||
|
### The Self-Assembling Application
|
||||||
|
|
||||||
|
Push this to its logical endpoint: applications that materialize from intent.
|
||||||
|
|
||||||
|
You describe what you want in natural language: "I want to help users plan healthy meals on a budget with easy recipes they can actually make."
|
||||||
|
|
||||||
|
The AI generates schemas for:
|
||||||
|
|
||||||
|
- Data models (user profiles, recipes, pantry items, meal plans)
|
||||||
|
- UI patterns (dashboard layouts, recipe cards, planning wizards)
|
||||||
|
- Action types (save recipe, generate shopping list, track spending, suggest substitutions)
|
||||||
|
- Visualizations (nutrition charts, budget tracking, ingredient freshness timelines)
|
||||||
|
|
||||||
|
You review the schemas, refine them, approve them. The frontend already knows how to render any valid schema. The backend already knows how to validate and store schema-conforming data.
|
||||||
|
|
||||||
|
What you're doing is no longer building features. You're curating and refining schemas. The system does the composition.
|
||||||
|
|
||||||
|
This sounds far-fetched until you realize we're already doing pieces of it. Code generation tools are getting better. Schema validation is mature. Component libraries are comprehensive. AI understands context remarkably well.
|
||||||
|
|
||||||
|
We're just connecting the pieces.
|
||||||
|
|
||||||
|
### Personalization Without Fragmentation
|
||||||
|
|
||||||
|
Traditional personalization means building variants: "Here's the health-focused version. Here's the budget-focused version. Here's the time-saving version."
|
||||||
|
|
||||||
|
You end up managing three codebases pretending to be one. Changes require updating all variants. Testing multiplies. Maintenance becomes painful.
|
||||||
|
|
||||||
|
Schema-driven personalization means generating the optimal interface for each user from the same underlying system.
|
||||||
|
|
||||||
|
User A cares about health. Their recipe cards prominently display nutrition information, macro breakdowns, ingredient quality scores, health impact summaries. The charts show nutrient density. The recommendations prioritize nutritional completeness.
|
||||||
|
|
||||||
|
User B cares about time. Their recipe cards show prep time first, active vs. passive time breakdowns, make-ahead options, batch cooking opportunities. The charts show time saved through meal prep. The recommendations prioritize efficiency.
|
||||||
|
|
||||||
|
User C cares about budget. Their recipe cards show cost per serving, bulk buying opportunities, seasonal ingredient savings, leftover utilization. The charts show cost comparisons. The recommendations prioritize affordability.
|
||||||
|
|
||||||
|
Same data. Same component library. Same schemas. Infinitely variable presentation based on what matters to each individual user.
|
||||||
|
|
||||||
|
No variant management. No A/B test fragments. No "which version am I in?" confusion. Just contextual generation.
|
||||||
|
|
||||||
|
And because it's schema-driven, you can combine dimensions. User D cares about both health and budget. They get nutrition data weighted by cost-effectiveness. User E cares about time and is also vegan. They get quick recipes that happen to be plant-based, not "vegan recipes" as a special category.
|
||||||
|
|
||||||
|
The combinations emerge from the generation logic, not from pre-built variants.
|
||||||
|
|
||||||
|
## The Trade-offs
|
||||||
|
|
||||||
|
This sounds exciting, and it is. But it's not free. The complexity moves around; it doesn't disappear.
|
||||||
|
|
||||||
|
### Schema Management Becomes Critical
|
||||||
|
|
||||||
|
Your schemas are your contract. They're what enables the whole system to work. Which means changing them is like changing your API.
|
||||||
|
|
||||||
|
You need versioning strategies. How do you evolve schemas without breaking existing data? How do you migrate old schema instances to new versions? How do you deprecate schema fields while maintaining backward compatibility?
|
||||||
|
|
||||||
|
You need synchronization mechanisms. The backend that generates schemas and the frontend that renders them must stay aligned. A mismatch means broken UI or validation errors.
|
||||||
|
|
||||||
|
You need discovery tools. As schemas multiply, developers need ways to find them, understand them, know which one to use for which scenario. Documentation becomes more important, not less.
|
||||||
|
|
||||||
|
This is real work. You might need a schema registry, similar to what you'd use for event-driven architectures. You might need automated testing that validates schema compatibility across versions. You might need tooling to generate TypeScript types from schemas and keep them in sync. Tools like Storybook become invaluable—you can document not just components, but how those components render different schema variations. Each story becomes a living example of "here's this schema shape, here's how it renders," making it easier for developers to understand which schema to use for which scenario.
|
||||||
|
|
||||||
|
Schema management is now a first-class concern in your architecture.
|
||||||
|
|
||||||
|
### Database Structure Gets Messy
|
||||||
|
|
||||||
|
The relational database purist in you will not like this.
|
||||||
|
|
||||||
|
Instead of neat columns with proper foreign keys and database-level constraints, you end up with JSONB blobs. The validation that would normally happen at the database layer moves to application code. Querying becomes harder—you can't easily "show me all high-priority compliance actions" when that data is buried in JSON.
|
||||||
|
|
||||||
|
There are workarounds. Hybrid models where you extract commonly-queried fields to columns and keep the dynamic data as JSON. PostgreSQL's JSONB type with indexes on frequently-accessed paths. Generated columns that pull specific values out of JSON for querying. Materialized views for complex queries that need to run often.
|
||||||
|
|
||||||
|
But it's more complex than traditional relational design. You're trading structure for flexibility.
|
||||||
|
|
||||||
|
The database is no longer your source of truth for what data is valid. Your schemas are. The database is just storage.
|
||||||
|
|
||||||
|
This is a philosophical shift as much as a technical one.
|
||||||
|
|
||||||
|
### Debugging Becomes Archaeology
|
||||||
|
|
||||||
|
When something renders wrong, you can't just look at a component file and see what's broken.
|
||||||
|
|
||||||
|
You need to trace: What was the user's context? What did the AI decide based on that context? What schema did it generate? Did it pass validation? How did the mapping layer interpret it? Which component got selected? What props were passed?
|
||||||
|
|
||||||
|
That's a lot of layers between "user saw wrong thing" and "here's the bug."
|
||||||
|
|
||||||
|
You need comprehensive logging at each step. You need replay tools that can take a saved context and schema and show you exactly how it rendered. You need schema versioning so you know which version of which schema generated which UI at which time. You need visibility into AI decision-making—why did it choose this action type over that one?
|
||||||
|
|
||||||
|
Debugging a static component is straightforward. Debugging a generated interface is detective work.
|
||||||
|
|
||||||
|
The tooling helps, but the conceptual overhead is real.
|
||||||
|
|
||||||
|
### Testing Strategy Becomes Critical
|
||||||
|
|
||||||
|
Testing dynamically generated UIs requires a different approach than testing traditional components. You can't just write unit tests for a component and call it done—the component itself might be simple, but the schema that drives it can vary infinitely.
|
||||||
|
|
||||||
|
Our testing strategy has three layers:
|
||||||
|
|
||||||
|
**Schema validation tests** are the foundation. Every schema gets comprehensive validation tests that verify the structure itself is correct. These tests catch issues like missing required fields, invalid enum values, or type mismatches. We treat schemas as first-class code artifacts with their own test suites.
|
||||||
|
|
||||||
|
**Contract tests** verify the relationship between schemas and components. Given a valid schema, does the component render without errors? We maintain a library of example schemas—edge cases, common patterns, minimal valid schemas—and run them through the rendering pipeline. This catches breaking changes when either schemas or components evolve.
|
||||||
|
|
||||||
|
**Integration tests** validate the full generation pipeline. We mock the AI responses with known schemas, then verify the entire flow: context analysis → schema generation → validation → rendering. This ensures the retry logic works, fallbacks activate correctly, and error boundaries catch failures gracefully.
|
||||||
|
|
||||||
|
We rely heavily on snapshot testing for the rendered output. When a schema changes, snapshot tests highlight exactly what UI changes resulted. This gives us confidence that schema evolution doesn't break existing interfaces unexpectedly.
|
||||||
|
|
||||||
|
The biggest shift is acceptance that you can't test every possible variation. Instead, you test the boundaries: minimum valid schemas, maximum complexity schemas, common patterns, and known edge cases. Type safety through Zod catches most issues at compile time, and runtime validation catches the rest before users see them.
|
||||||
|
|
||||||
|
### Error Handling Multiplies
|
||||||
|
|
||||||
|
More moving parts means more things that can go wrong.
|
||||||
|
|
||||||
|
AI generation can fail. Network timeout, rate limit, malformed response. Validation can fail. AI generated something that doesn't match the schema. Retry loop can exhaust. After three attempts, still no valid schema. Rendering can fail. Unknown schema type, missing required data, component error.
|
||||||
|
|
||||||
|
You need graceful fallbacks for every failure mode.
|
||||||
|
|
||||||
|
Default schemas that are safe to show when generation fails. Error boundaries in the frontend that catch rendering failures and show something useful. Monitoring and alerting for validation failures so you know when your prompts or schemas need adjustment. User-friendly error states—"We're having trouble generating recommendations right now, here's what we suggest generally..."
|
||||||
|
|
||||||
|
Every dynamic system trades simplicity for resilience engineering.
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
|
||||||
|
AI generation adds latency. LLM API calls can take seconds. You can't generate fresh schemas on every page load.
|
||||||
|
|
||||||
|
You need strategies: pre-generation for common scenarios, caching generated schemas by context hash, background generation with optimistic UI, hybrid approaches where you show static defaults while dynamic enhancement loads, edge computing to move generation closer to users.
|
||||||
|
|
||||||
|
But you're adding complexity to maintain responsiveness. The simple "render component with props" is now "check cache, maybe generate, validate, render, handle failure cases."
|
||||||
|
|
||||||
|
The performance budget gets spent differently.
|
||||||
|
|
||||||
|
### Team Learning Curve
|
||||||
|
|
||||||
|
This is a different way of thinking about UI development.
|
||||||
|
|
||||||
|
Your team needs to understand schema design—what makes a good schema, how to evolve them, how to version them. They need to understand runtime validation and how to write schemas that AI can reliably generate. They need to understand slot-based architecture and mapping layers. They need to understand the trade-offs between flexibility and predictability.
|
||||||
|
|
||||||
|
Not every team wants this complexity. For simple, predictable applications, it's overkill. The traditional component-per-feature approach works fine when features are truly unique and don't follow patterns.
|
||||||
|
|
||||||
|
You're choosing a different set of problems. More upfront design work on schemas, less repetitive component building. More system-level thinking, less feature-level implementation.
|
||||||
|
|
||||||
|
It's not objectively better. It's different. And it requires buy-in.
|
||||||
|
|
||||||
|
## When Does This Make Sense?
|
||||||
|
|
||||||
|
So when should you actually consider this approach?
|
||||||
|
|
||||||
|
**Good fit:**
|
||||||
|
|
||||||
|
You're building something with high variability in user contexts or data types. The same underlying features need to look different for different users, different roles, different situations.
|
||||||
|
|
||||||
|
You have frequent new requirements that follow similar patterns. Not "build a completely new feature," but "this feature needs to work slightly differently for this new context."
|
||||||
|
|
||||||
|
Your domain is one where AI can make intelligent contextual decisions. There are patterns to learn, data to analyze, reasonable inferences to make.
|
||||||
|
|
||||||
|
Your team is comfortable with schema-driven development. Or willing to learn. And has the capacity to manage the additional architectural complexity.
|
||||||
|
|
||||||
|
Your users benefit meaningfully from personalized, adaptive experiences. The variability actually matters to them; it's not just engineer preference.
|
||||||
|
|
||||||
|
**Bad fit:**
|
||||||
|
|
||||||
|
You have pixel-perfect design requirements. Brand campaigns, marketing sites, anything where the exact visual presentation is non-negotiable. Schema-driven generation gives you flexibility, not precision.
|
||||||
|
|
||||||
|
Your feature set is predictable and stable. If you're building ten truly unique features with no pattern between them, there's no schema to extract. Just build the ten features.
|
||||||
|
|
||||||
|
You have a small team without capacity for schema management. The overhead might outweigh the benefits.
|
||||||
|
|
||||||
|
You're working on performance-critical real-time interactions. The latency of generation and validation might not be acceptable.
|
||||||
|
|
||||||
|
You have regulatory requirements for fixed UI flows. Sometimes the law requires specific workflows in specific orders. Dynamic generation adds compliance risk.
|
||||||
|
|
||||||
|
**The litmus test:** If you find yourself building slight variations of the same component over and over—same structure, different data, different actions, different styling—you're a candidate. You have patterns worth extracting into schemas.
|
||||||
|
|
||||||
|
If every feature is truly unique, truly bespoke, you're probably not.
|
||||||
|
|
||||||
|
## Components Learning to Think
|
||||||
|
|
||||||
|
Component libraries aren't dead. They're evolving.
|
||||||
|
|
||||||
|
We're moving from tools we manually compose to systems that compose themselves based on context.
|
||||||
|
|
||||||
|
What we built at Bambee is proof that this works in production. Dynamic wizards that adapt to what information is missing. Adaptive cards that surface different insights for different users. Contextual visualizations that show what matters most. All using our component library—Nuxt UI—just arranged by the system instead of by developers.
|
||||||
|
|
||||||
|
But it's early. The schemas are still simple. The generation logic is straightforward. The mapping layers are manageable.
|
||||||
|
|
||||||
|
What comes next is more interesting.
|
||||||
|
|
||||||
|
More sophisticated schemas that can express complex layouts, responsive variations, accessibility requirements, animation preferences. Cross-domain schema standards that let us share UI patterns across completely different applications. Entire pages assembled from intent rather than files. Vector-powered asset selection that makes every interface feel custom. Progressive complexity that adapts to user capability in real-time. Mass personalization without the maintenance burden of variants.
|
||||||
|
|
||||||
|
The vision: developers define schemas and provide component libraries. AI generates optimal UI for each user's context. Users see interfaces that feel custom-built for them. No custom building required.
|
||||||
|
|
||||||
|
This isn't replacing the craft of UI development. It's augmenting it. Moving us from pixel-pushing to pattern-defining. From building variations to building systems that generate variations. From asking "how do I build this interface?" to asking "how do I describe the space of possible interfaces?"
|
||||||
|
|
||||||
|
The future of UI development might not be about building interfaces. It might be about building the systems that build interfaces.
|
||||||
|
|
||||||
|
Component libraries aren't dying. They're learning to think.
|
||||||
244
.firecrawl/byteiota-vite8.md
Normal file
244
.firecrawl/byteiota-vite8.md
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
Share
|
||||||
|
|
||||||
|
- [Share on Facebook](https://www.facebook.com/sharer.php?u=https%3A%2F%2Fbyteiota.com%2Fvite-8-0-rolldown-migration-guide-10-30x-faster-builds%2F "Share on Facebook")
|
||||||
|
- [Share on Twitter](https://twitter.com/share?url=https%3A%2F%2Fbyteiota.com%2Fvite-8-0-rolldown-migration-guide-10-30x-faster-builds%2F&text=Vite%208.0%20Rolldown%20Migration%20Guide:%2010-30x%20Faster%20Builds "Share on Twitter")
|
||||||
|
|
||||||
|
- [Share on Linkedin](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fbyteiota.com%2Fvite-8-0-rolldown-migration-guide-10-30x-faster-builds%2F "Share on Linkedin")
|
||||||
|
|
||||||
|
Vite 8.0 stable dropped on March 12, 2026, replacing its dual-bundler architecture with [Rolldown](https://rolldown.rs/), a single Rust-based bundler delivering 10-30x faster builds. Linear saw production builds shrink from 46 seconds to 6 seconds—an 87% reduction. With Vite downloaded 65 million times weekly, this upgrade affects millions of developers. Here’s your migration guide.
|
||||||
|
|
||||||
|
## Vite 8 Unifies Build Pipeline with Rolldown
|
||||||
|
|
||||||
|
Vite 8 consolidates two bundlers into one. Previously, Vite used esbuild for fast development and Rollup for optimized production builds. This dual-bundler approach worked but created potential inconsistencies between dev and prod environments. Rolldown eliminates that split.
|
||||||
|
|
||||||
|
Rolldown is a Rust-based bundler with full Rollup API compatibility. It matches esbuild’s development speed while delivering 10-30x faster production builds than Rollup. In official benchmarks testing 19,000 modules, Rolldown completed in 1.61 seconds versus Rollup’s 40.10 seconds—a 25x improvement.
|
||||||
|
|
||||||
|
The architectural unification simplifies configuration. Developers no longer juggle separate esbuild and rollupOptions settings. One bundler, one config, consistent behavior across environments.
|
||||||
|
|
||||||
|
## 10-30x Faster Builds in Real-World Projects
|
||||||
|
|
||||||
|
Performance gains are substantial. [Linear reduced production build times by 87%](https://vite.dev/blog/announcing-vite8), dropping from 46 seconds to 6 seconds. Beehiiv’s large codebase saw 64% improvement. Mercedes-Benz.io cut build times by 38%.
|
||||||
|
|
||||||
|
However, performance gains scale with project size. Small projects under 100 modules see 2-5x improvements. Mid-sized projects between 100-500 modules hit 5-10x. Large projects with 500+ modules achieve the advertised 10-30x gains.
|
||||||
|
|
||||||
|
One developer testing a single-page app watched builds shrink from 3.8 seconds to 0.8 seconds—a clean 5x improvement. For large enterprise apps, these savings multiply across hundreds of daily builds, cutting hours from CI/CD pipelines.
|
||||||
|
|
||||||
|
## How to Migrate to Vite 8
|
||||||
|
|
||||||
|
Migration is straightforward for most projects. Update Vite to 8.0, test locally, deploy if no issues arise. A compatibility layer auto-converts esbuild and rollupOptions configurations to Rolldown equivalents.
|
||||||
|
|
||||||
|
Basic migration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update to Vite 8.0
|
||||||
|
npm install vite@8
|
||||||
|
|
||||||
|
# Test development server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Test production build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Benchmark performance
|
||||||
|
time npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Most projects work without configuration changes. The compatibility layer handles the transition automatically. If you encounter issues, [Vite provides a gradual migration path](https://main.vite.dev/guide/migration): test with rolldown-vite on Vite 7 first, then upgrade to Vite 8 stable. This two-step approach isolates Rolldown-specific problems from general upgrade issues.
|
||||||
|
|
||||||
|
## Should You Upgrade? Decision Framework
|
||||||
|
|
||||||
|
Upgrade priority depends on project size and build frequency. Large codebases with 500+ modules benefit most—10-30x gains justify immediate migration. Teams running multiple builds daily see compounding time savings. If CI/CD pipelines take 40 seconds per build, Rolldown cuts that to 2 seconds, saving 38 seconds × 100 builds = 63 minutes daily.
|
||||||
|
|
||||||
|
Mid-sized projects between 100-500 modules should upgrade within the month. You’ll see 5-10x improvements—noticeable but not game-changing. Standard release cycles (daily or weekly deploys) make this a medium priority.
|
||||||
|
|
||||||
|
Small projects under 100 modules see 2-5x gains. Still worthwhile, but less impactful. If you’re risk-averse or running mission-critical production apps, waiting 1-2 months for community feedback is reasonable. Let others find the edge cases first.
|
||||||
|
|
||||||
|
Skip immediate upgrade if you rely on obscure Rollup plugins that may lack Rolldown compatibility. Check the Vite plugin registry first. Also skip if you’re on Vite 6 or older—address that gap before jumping to Vite 8.
|
||||||
|
|
||||||
|
## Troubleshooting Common Migration Issues
|
||||||
|
|
||||||
|
Three issues account for most migration headaches: CommonJS interop changes, manualChunks deprecation, and esbuild transform failures.
|
||||||
|
|
||||||
|
CommonJS imports may break due to Rolldown’s stricter module handling. If runtime errors appear when importing CJS modules, add `legacy.inconsistentCjsInterop: true` to your config temporarily. Long-term, migrate to ESM or fix module resolution. This isn’t Vite’s fault—it’s exposing existing module system inconsistencies.
|
||||||
|
|
||||||
|
The `manualChunks` config no longer works. Vite 8 uses `codeSplitting` instead, offering more granular control:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// OLD (Vite 7)
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (/\/react(?:-dom)?/.test(id)) {
|
||||||
|
return 'vendor'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// NEW (Vite 8)
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
rolldownOptions: {
|
||||||
|
output: {
|
||||||
|
codeSplitting: {
|
||||||
|
groups: [\
|
||||||
|
{ name: 'vendor', test: /\/react(?:-dom)?/ }\
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugins using `transformWithEsbuild` will fail because esbuild is no longer bundled. Migrate to `transformWithOxc` or install esbuild manually as a peer dependency. The @vitejs/plugin-react v6 already made this switch, using Oxc for React Refresh transforms and eliminating the Babel dependency entirely.
|
||||||
|
|
||||||
|
## What’s Next for Vite
|
||||||
|
|
||||||
|
Vite’s roadmap includes Full Bundle Mode (experimental), which serves bundled files in development for 3× faster dev server startup, 40% faster full reloads, and 10× fewer network requests. This addresses one of Vite’s last pain points—hundreds of separate module requests in large apps.
|
||||||
|
|
||||||
|
[VoidZero](https://voidzero.dev/posts/announcing-rolldown-rc), the team behind Vite and Rolldown, is building a unified Rust toolchain for JavaScript development. Rolldown handles bundling, Oxc powers compilation and minification, and more tools are coming. The trend is clear: Rust-based tooling is replacing JavaScript-based build tools across the ecosystem, from swc to turbopack to Biome.
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
- Vite 8.0 stable replaces esbuild + Rollup with a single Rust bundler (Rolldown), delivering 10-30x faster production builds while maintaining plugin compatibility
|
||||||
|
- Large projects (500+ modules) see the biggest gains (10-30x), mid-sized projects hit 5-10x, small projects get 2-5x—performance scales with codebase size
|
||||||
|
- Most projects migrate without config changes thanks to auto-conversion, but CommonJS interop and manualChunks require manual fixes
|
||||||
|
- Upgrade now if you’re on Vite 7 with large codebases or frequent builds—the stable release is production-ready and compatibility is high
|
||||||
|
- Future Vite improvements (Full Bundle Mode, Rust toolchain expansion) show continued innovation, making this a safe long-term bet
|
||||||
|
|
||||||
|
Vite 8 isn’t just faster—it’s simpler. One bundler, one config, one mental model. The dual-bundler era is over.
|
||||||
|
|
||||||
|
### Share
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[ByteBot](https://byteiota.com/author/bytebot/ "Posts by ByteBot")
|
||||||
|
|
||||||
|
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.
|
||||||
|
|
||||||
|
[Previous](https://byteiota.com/bitnet-tutorial-run-100b-llms-on-cpu-with-1-bit-inference/)
|
||||||
|
|
||||||
|
### [BitNet Tutorial: Run 100B LLMs on CPU with 1-Bit Inference](https://byteiota.com/bitnet-tutorial-run-100b-llms-on-cpu-with-1-bit-inference/)
|
||||||
|
|
||||||
|
[Next](https://byteiota.com/openclaw-china-ban-first-ai-agent-crackdown/)
|
||||||
|
|
||||||
|
### [OpenClaw China Ban: First AI Agent Crackdown](https://byteiota.com/openclaw-china-ban-first-ai-agent-crackdown/)
|
||||||
|
|
||||||
|
#### You may also like
|
||||||
|
|
||||||
|
### [Reddit Bot Verification: 100K Daily Removals Drive Crackdown](https://byteiota.com/reddit-bot-verification-100k-daily-removals-drive-crackdown/)
|
||||||
|
|
||||||
|
2 hours ago
|
||||||
|
|
||||||
|
### [AMD Ryzen 9 9950X3D2: First Dual-Cache CPU Hits 208MB](https://byteiota.com/amd-ryzen-9-9950x3d2-first-dual-cache-cpu-hits-208mb/)
|
||||||
|
|
||||||
|
4 hours ago
|
||||||
|
|
||||||
|
[](https://byteiota.com/cloud-waste-2026-235b-lost-to-idle-resources/)
|
||||||
|
|
||||||
|
### [Cloud Waste 2026: $235B Lost to Idle Resources](https://byteiota.com/cloud-waste-2026-235b-lost-to-idle-resources/)
|
||||||
|
|
||||||
|
5 hours ago
|
||||||
|
|
||||||
|
[](https://byteiota.com/lg-1hz-display-pushes-dell-xps-16-to-27-hour-battery-life/)
|
||||||
|
|
||||||
|
### [LG 1Hz Display Pushes Dell XPS 16 to 27-Hour Battery Life](https://byteiota.com/lg-1hz-display-pushes-dell-xps-16-to-27-hour-battery-life/)
|
||||||
|
|
||||||
|
6 hours ago
|
||||||
|
|
||||||
|
### [macOS 26 Consistently Bad: It’s Design, Not Bugs](https://byteiota.com/macos-26-consistently-bad-its-design-not-bugs/)
|
||||||
|
|
||||||
|
8 hours ago
|
||||||
|
|
||||||
|
[](https://byteiota.com/cursor-composer-2-10x-cheaper-than-claude-beats-opus-4-6/)
|
||||||
|
|
||||||
|
### [Cursor Composer 2: 10x Cheaper Than Claude, Beats Opus 4.6](https://byteiota.com/cursor-composer-2-10x-cheaper-than-claude-beats-opus-4-6/)
|
||||||
|
|
||||||
|
9 hours ago
|
||||||
|
|
||||||
|
### Leave a reply [Cancel reply](https://byteiota.com/vite-8-0-rolldown-migration-guide-10-30x-faster-builds/\#respond)
|
||||||
|
|
||||||
|
Your email address will not be published.Required fields are marked \*
|
||||||
|
|
||||||
|
Comment
|
||||||
|
|
||||||
|
Name \*
|
||||||
|
|
||||||
|
Email \*
|
||||||
|
|
||||||
|
Website
|
||||||
|
|
||||||
|
Save my name, email, and website in this browser for the next time I comment.
|
||||||
|
|
||||||
|
Δ
|
||||||
|
|
||||||
|
#### More in: [JavaScript](https://byteiota.com/programming/javascript/)
|
||||||
|
|
||||||
|
[](https://byteiota.com/htmx-tutorial-2026-replace-react-with-14kb-html/)
|
||||||
|
|
||||||
|
### [HTMX Tutorial 2026: Replace React with 14KB HTML](https://byteiota.com/htmx-tutorial-2026-replace-react-with-14kb-html/)
|
||||||
|
|
||||||
|
5 days ago
|
||||||
|
|
||||||
|
[](https://byteiota.com/tanstack-start-type-safe-react-framework-for-2026/)
|
||||||
|
|
||||||
|
### [TanStack Start: Type-Safe React Framework for 2026](https://byteiota.com/tanstack-start-type-safe-react-framework-for-2026/)
|
||||||
|
|
||||||
|
5 days ago
|
||||||
|
|
||||||
|
[](https://byteiota.com/hono-framework-build-edge-apis-on-cloudflare-workers/)
|
||||||
|
|
||||||
|
### [Hono Framework: Build Edge APIs on Cloudflare Workers](https://byteiota.com/hono-framework-build-edge-apis-on-cloudflare-workers/)
|
||||||
|
|
||||||
|
6 days ago
|
||||||
|
|
||||||
|
[](https://byteiota.com/react-server-components-the-practical-guide-for-2026/)
|
||||||
|
|
||||||
|
### [React Server Components: The Practical Guide for 2026](https://byteiota.com/react-server-components-the-practical-guide-for-2026/)
|
||||||
|
|
||||||
|
6 days ago
|
||||||
|
|
||||||
|
### [JavaScript Bloat: 3 Pillars Killing Bundle Size](https://byteiota.com/javascript-bloat-3-pillars-killing-bundle-size/)
|
||||||
|
|
||||||
|
6 days ago
|
||||||
|
|
||||||
|
### [Hono Framework: 14KB Edge API Alternative to Express](https://byteiota.com/hono-framework-14kb-edge-api-alternative-to-express/)
|
||||||
|
|
||||||
|
March 13, 2026
|
||||||
|
|
||||||
|
Next Article:
|
||||||
|
|
||||||
|
March 13, 2026
|
||||||
|
|
||||||
|
min read
|
||||||
|
|
||||||
|
-21 %
|
||||||
|
|
||||||
|
## [](https://byteiota.com/)
|
||||||
|
|
||||||
|
[✕Close](https://byteiota.com/vite-8-0-rolldown-migration-guide-10-30x-faster-builds/#atbs-ceris-offcanvas-primary)
|
||||||
|
|
||||||
|
## [](https://byteiota.com/)
|
||||||
|
|
||||||
|
[✕](https://byteiota.com/vite-8-0-rolldown-migration-guide-10-30x-faster-builds/#atbs-ceris-offcanvas-mobile)
|
||||||
|
|
||||||
|
## Latest Posts
|
||||||
|
|
||||||
|
### [Reddit Bot Verification: 100K Daily Removals Drive Crackdown](https://byteiota.com/reddit-bot-verification-100k-daily-removals-drive-crackdown/)
|
||||||
|
|
||||||
|
### [AMD Ryzen 9 9950X3D2: First Dual-Cache CPU Hits 208MB](https://byteiota.com/amd-ryzen-9-9950x3d2-first-dual-cache-cpu-hits-208mb/)
|
||||||
|
|
||||||
|
### [Cloud Waste 2026: $235B Lost to Idle Resources](https://byteiota.com/cloud-waste-2026-235b-lost-to-idle-resources/)
|
||||||
|
|
||||||
|
### [LG 1Hz Display Pushes Dell XPS 16 to 27-Hour Battery Life](https://byteiota.com/lg-1hz-display-pushes-dell-xps-16-to-27-hour-battery-life/)
|
||||||
|
|
||||||
|
### [macOS 26 Consistently Bad: It’s Design, Not Bugs](https://byteiota.com/macos-26-consistently-bad-its-design-not-bugs/)
|
||||||
|
|
||||||
|
[](https://feedmatters.com/?utm_source=byteiota&utm_medium=banner&utm_campaign=popup)
|
||||||
|
|
||||||
|
×
|
||||||
112
.firecrawl/corvu-dynamic.md
Normal file
112
.firecrawl/corvu-dynamic.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# Dynamic Components
|
||||||
|
|
||||||
|
All primitive components that render a DOM element are dynamic, which means that you can modify the element or the component they should render as.
|
||||||
|
|
||||||
|
## Native elements [Section titled Native elements](https://corvu.dev/docs/dynamic-components/\#native-elements)
|
||||||
|
|
||||||
|
In most cases, you shouldn’t need to change the DOM element that the primitive component renders. corvu has sensible defaults for all components. But there are cases where it makes sense to change them. An example would be the `Tooltip` trigger which renders as a `button` element. You may want to render a tooltip on a link (`a` tag) instead. To do this, you have to specify the `as` property on the trigger component:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Tooltip.Trigger as="a" href="https://corvu.dev">
|
||||||
|
corvu.dev
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solid components [Section titled Solid components](https://corvu.dev/docs/dynamic-components/\#solid-components)
|
||||||
|
|
||||||
|
A much more common use case is to render a primitive component as a custom Solid component. This is useful to apply default styling or to add additional functionality.
|
||||||
|
|
||||||
|
For example, you might have your own, custom-styled button component and want to use it as a trigger for a dialog:
|
||||||
|
|
||||||
|
```
|
||||||
|
import {
|
||||||
|
ComponentProps,
|
||||||
|
splitProps,
|
||||||
|
} from 'solid-js'
|
||||||
|
import Dialog from '@corvu/dialog'
|
||||||
|
|
||||||
|
const CustomButton = (
|
||||||
|
props: ComponentProps<'button'> & { variant: 'fill' | 'outline' },
|
||||||
|
) => {
|
||||||
|
const [local, rest] = splitProps(props, ['variant'])
|
||||||
|
// Apply your custom styling here
|
||||||
|
return <button class={local.variant} {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogTrigger = () => (
|
||||||
|
<Dialog.Trigger as={CustomButton} variant="outline">
|
||||||
|
Open
|
||||||
|
</Dialog.Trigger>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Props not belonging to the primitive component will be passed through to your custom component. In this case, the `variant` prop is passed to the `CustomButton` component.
|
||||||
|
|
||||||
|
> ❗ To ensure functionality and accessibility, your component needs to spread the received props onto your element. Otherwise, corvu can’t define props on the element and things will break.
|
||||||
|
|
||||||
|
## Component types [Section titled Component types](https://corvu.dev/docs/dynamic-components/\#component-types)
|
||||||
|
|
||||||
|
corvu’s dynamic components have a flexible type system. This allows library developers or users who want to create their own components based on corvu’s primitives to have a great developer experience.
|
||||||
|
|
||||||
|
Every dynamic component exposes 4 types.
|
||||||
|
|
||||||
|
For example, the `<Dialog.Trigger>` component exports the types `DialogTriggerCorvuProps`, `DialogTriggerSharedElementProps<T>`, `DialogTriggerElementProps` and `DialogTriggerProps<T>`.
|
||||||
|
|
||||||
|
Lets have a look at the types:
|
||||||
|
|
||||||
|
### CorvuProps [Section titled CorvuProps](https://corvu.dev/docs/dynamic-components/\#corvuprops)
|
||||||
|
|
||||||
|
`CorvuProps` contains all props that are specific to the primitive component and are not passed through to the rendered element. They are consumed by corvu. For example, the [`<Resizable.Handle />`](https://corvu.dev/docs/primitives/resizable/#Panel) has props like `minSize` or `collapsible`.
|
||||||
|
|
||||||
|
### SharedElementProps<T extends ValidComponent> [Section titled SharedElementProps<T extends ValidComponent>](https://corvu.dev/docs/dynamic-components/\#sharedelementpropst-extends-validcomponent)
|
||||||
|
|
||||||
|
`SharedElementProps` includes all props that get defined by corvu on the rendered element **but** can be overridden by the user. This usually includes the `ref` or `style` properties. The generic is used to properly type `ref` and event listeners.
|
||||||
|
|
||||||
|
### ElementProps [Section titled ElementProps](https://corvu.dev/docs/dynamic-components/\#elementprops)
|
||||||
|
|
||||||
|
`ElementProps` element props inherits all `SharedElementProps` and additionally includes all props that are set by corvu and can’t be overridden by the user. This includes for example accessibility props like `aria-*`, `role` and `data-*`.
|
||||||
|
|
||||||
|
### Props<T extends ValidComponent> [Section titled Props<T extends ValidComponent>](https://corvu.dev/docs/dynamic-components/\#propst-extends-validcomponent)
|
||||||
|
|
||||||
|
This is the type that defines what props that corvu expects from the user. It’s equal to `CorvuProps & Partial<SharedElementProps>`.
|
||||||
|
|
||||||
|
### DynamicProps [Section titled DynamicProps](https://corvu.dev/docs/dynamic-components/\#dynamicprops)
|
||||||
|
|
||||||
|
`DynamicProps` is a helper type that allows you to expose the dynamic aspect of corvu components from your custom component. Let’s look at an example:
|
||||||
|
|
||||||
|
```
|
||||||
|
import {
|
||||||
|
type ValidComponent,
|
||||||
|
ComponentProps,
|
||||||
|
splitProps,
|
||||||
|
} from 'solid-js'
|
||||||
|
import Dialog, { type TriggerProps, type DynamicProps } from '@corvu/dialog'
|
||||||
|
|
||||||
|
// Define your custom props, including `TriggerProps` from corvu
|
||||||
|
export type CustomDisclosureTriggerProps<T extends ValidComponent = 'button'> = TriggerProps<T> & {
|
||||||
|
variant: 'fill' | 'outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
// The generic `T` allows the user to specify
|
||||||
|
// the element this component should render as
|
||||||
|
const CustomDialogTrigger = <T extends ValidComponent = 'button'>(
|
||||||
|
props: DynamicProps<T, CustomDisclosureTriggerProps<T>>,
|
||||||
|
) => {
|
||||||
|
const [local, rest] = splitProps(props as CustomDisclosureTriggerProps, [\
|
||||||
|
'variant',\
|
||||||
|
])
|
||||||
|
// Define the default dynamic type, in this case 'button'.
|
||||||
|
// This can be overridden by the user.
|
||||||
|
return <Dialog.Trigger as="button" class={local.variant} {...rest} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don’t want to expose the dynamic aspect of corvu’s components, you can define the generic explicitly:
|
||||||
|
|
||||||
|
```
|
||||||
|
const CustomDialogTrigger = (
|
||||||
|
props: DynamicProps<'button', CustomDisclosureTriggerProps<'button'>>,
|
||||||
|
) => {
|
||||||
|
```
|
||||||
|
|
||||||
|
Developed and designed by [Jasmin](https://github.com/GiyoMoon/)
|
||||||
15
.firecrawl/corvu-overview.md
Normal file
15
.firecrawl/corvu-overview.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Overview
|
||||||
|
|
||||||
|
## Primitives [Section titled Primitives](https://corvu.dev/docs/overview/\#primitives)
|
||||||
|
|
||||||
|
corvu’s growing list of UI primitives for SolidJS. Accessible, customizable and ready to use in your project!
|
||||||
|
|
||||||
|
[Accordion](https://corvu.dev/docs/primitives/accordion/) [Calendar](https://corvu.dev/docs/primitives/calendar/) [Dialog](https://corvu.dev/docs/primitives/dialog/) [Disclosure](https://corvu.dev/docs/primitives/disclosure/) [Drawer](https://corvu.dev/docs/primitives/drawer/) [OTP Field](https://corvu.dev/docs/primitives/otp-field/) [Popover](https://corvu.dev/docs/primitives/popover/) [Resizable](https://corvu.dev/docs/primitives/resizable/) [Tooltip](https://corvu.dev/docs/primitives/tooltip/)
|
||||||
|
|
||||||
|
## Utilities [Section titled Utilities](https://corvu.dev/docs/overview/\#utilities)
|
||||||
|
|
||||||
|
A set of commonly needed, low-level patterns that you might need in the modern, accessible web. They are exported as separate packages and can be used independently from corvu.
|
||||||
|
|
||||||
|
[dismissible](https://corvu.dev/docs/utilities/dismissible/) [focusTrap](https://corvu.dev/docs/utilities/focus-trap/) [list](https://corvu.dev/docs/utilities/list/) [persistent](https://corvu.dev/docs/utilities/persistent/) [presence](https://corvu.dev/docs/utilities/presence/) [preventScroll](https://corvu.dev/docs/utilities/prevent-scroll/) [transitionSize](https://corvu.dev/docs/utilities/transition-size/)
|
||||||
|
|
||||||
|
Developed and designed by [Jasmin](https://github.com/GiyoMoon/)
|
||||||
125
.firecrawl/corvu-state.md
Normal file
125
.firecrawl/corvu-state.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# State
|
||||||
|
|
||||||
|
By default, all corvu primitives manage their state internally. This means that they handle their state themselves and you don’t need to pass any props/state to them.
|
||||||
|
|
||||||
|
corvu aims to be very customizable and provides various ways to control a primitive or access internal state. Let’s take a look at them. We’ll use the [Dialog](https://corvu.dev/docs/primitives/dialog/) primitive as an example, but the same applies to all other primitives.
|
||||||
|
|
||||||
|
## Controlled State [Section titled Controlled State](https://corvu.dev/docs/state/\#controlled-state)
|
||||||
|
|
||||||
|
The easiest way to control a primitive’s state is by passing your own defined state to its `Root` component. Most of the time, this consists of a getter and setter property like in this example, where we control the open state of a dialog:
|
||||||
|
|
||||||
|
```
|
||||||
|
import Dialog from '@corvu/dialog'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
|
||||||
|
const MyDialog = () => {
|
||||||
|
const [open, setOpen] = createSignal(false)
|
||||||
|
return (
|
||||||
|
<Dialog open={open()} onOpenChange={setOpen}>
|
||||||
|
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Label>Label</Dialog.Label>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows you to use the open state anywhere in your code and alter it freely. The dialog will open and close accordingly.
|
||||||
|
|
||||||
|
## Context [Section titled Context](https://corvu.dev/docs/state/\#context)
|
||||||
|
|
||||||
|
It’s also possible to access the context of every primitive. This allows you to get the internal state of a primitive from anywhere in your code, as long as it’s under the `Root` component, where the context gets provided. Accessing the context of a dialog looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
import Dialog from '@corvu/dialog'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
|
||||||
|
const DialogRoot = () => {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogContent />
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogContent = () => {
|
||||||
|
const { open, setOpen } = Dialog.useContext()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>The dialog is {open() ? 'open' : 'closed'}</p>
|
||||||
|
<button onClick={() => setOpen(open => !open)}>
|
||||||
|
My own, custom trigger button
|
||||||
|
</button>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Label>Label</Dialog.Label>
|
||||||
|
</Dialog.Content>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every primitive provides different properties in its context, have a look at the API section of each primitive to see what’s available.
|
||||||
|
|
||||||
|
## Children callbacks [Section titled Children callbacks](https://corvu.dev/docs/state/\#children-callbacks)
|
||||||
|
|
||||||
|
The `Root` component of every primitive (and in a few cases others) also accepts a function as its children. By doing this, we can pass the internal state to the children for you to access. An example of this looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
import Dialog from '@corvu/dialog'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
|
||||||
|
const MyDialog = () => {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
{(props) => (
|
||||||
|
<>
|
||||||
|
<p>The dialog is {props.open() ? 'open' : 'closed'}</p>
|
||||||
|
<button onClick={() => props.setOpen(open => !open)}>
|
||||||
|
My own, custom trigger button
|
||||||
|
</button>
|
||||||
|
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Label>Label</Dialog.Label>
|
||||||
|
</Dialog.Content>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the props passed from the `Root` component include reactive getters. Make sure to access them in a reactive scope, like you would in any other Solid component.
|
||||||
|
|
||||||
|
## Keyed context [Section titled Keyed context](https://corvu.dev/docs/state/\#keyed-context)
|
||||||
|
|
||||||
|
There may be situations where you want to use nested instances of the same primitive. For example, multiple dialogs with multiple trigger buttons that are hard to separate in the template. For this case, corvu allows you to pass a `contextId` to every primitive component to tell which context to use.
|
||||||
|
|
||||||
|
Here’s how two nested dialogs would look like:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Dialog contextId="dialog-1">
|
||||||
|
<Dialog contextId="dialog-2">
|
||||||
|
|
||||||
|
<Dialog.Trigger contextId="dialog-1">Open Dialog 1</Dialog.Trigger>
|
||||||
|
<Dialog.Trigger contextId="dialog-2">Open Dialog 2</Dialog.Trigger>
|
||||||
|
|
||||||
|
<Dialog.Content contextId="dialog-1">
|
||||||
|
<Dialog.Label contextId="dialog-1">Dialog 1</Dialog.Label>
|
||||||
|
</Dialog.Content>
|
||||||
|
|
||||||
|
<Dialog.Content contextId="dialog-2">
|
||||||
|
<Dialog.Label contextId="dialog-2">Dialog 2</Dialog.Label>
|
||||||
|
</Dialog.Content>
|
||||||
|
|
||||||
|
</Dialog>
|
||||||
|
</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
When using keyed contexts, you can pass the same key to the `useContext()` function to access the context of the respective primitive.
|
||||||
|
|
||||||
|
```
|
||||||
|
const { open, setOpen } = Dialog.useContext('dialog-1')
|
||||||
|
```
|
||||||
|
|
||||||
|
Developed and designed by [Jasmin](https://github.com/GiyoMoon/)
|
||||||
91
.firecrawl/corvu-styling.md
Normal file
91
.firecrawl/corvu-styling.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Styling
|
||||||
|
|
||||||
|
corvu leaves the styling up to you. You can use Tailwind CSS, any CSS-in-JS library or just plain old CSS to style primitives.
|
||||||
|
|
||||||
|
## Data attributes [Section titled Data attributes](https://corvu.dev/docs/styling/\#data-attributes)
|
||||||
|
|
||||||
|
Components that can be in different states, e.g. `open` or `closed` for a dialog, provide data attributes to style them accordingly.
|
||||||
|
|
||||||
|
Here is an example of how to style a dialog based on its open state:
|
||||||
|
|
||||||
|
```
|
||||||
|
.dialog_content[data-open] {
|
||||||
|
/* styles to apply when open */
|
||||||
|
}
|
||||||
|
.dialog_content[data-closed] {
|
||||||
|
/* styles to apply when closed */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Don’t forget to add the `dialog_content` class to your Dialog content component:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Dialog.Content class="dialog_content">...</Dialog.Content>
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, every corvu component has a data attribute for you to use. A dialog content element would render like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
<div data-corvu-dialog-content data-open>...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use it to style all components of the same kind at once:
|
||||||
|
|
||||||
|
```
|
||||||
|
[data-corvu-dialog-content] {
|
||||||
|
/* styles to apply to the dialog content */
|
||||||
|
}
|
||||||
|
[data-corvu-dialog-content][data-open] {
|
||||||
|
/* styles to apply when open */
|
||||||
|
}
|
||||||
|
[data-corvu-dialog-content][data-closed] {
|
||||||
|
/* styles to apply when closed */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, corvu provides plugins for these CSS frameworks:
|
||||||
|
|
||||||
|
- [Tailwind CSS plugin](https://corvu.dev/docs/installation/#tailwind-css-plugin)
|
||||||
|
- [UnoCSS preset](https://corvu.dev/docs/installation/#unocss-preset)
|
||||||
|
|
||||||
|
They make it easy to style components based on their current state using modifiers.
|
||||||
|
|
||||||
|
**Available modifiers**
|
||||||
|
|
||||||
|
- `corvu-open` -\> `&[data-open]`
|
||||||
|
- `corvu-closed` -\> `&[data-closed]`
|
||||||
|
- `corvu-expanded` -\> `&[data-expanded]`
|
||||||
|
- `corvu-collapsed` -\> `&[data-collapsed]`
|
||||||
|
- `corvu-transitioning` -\> `&[data-transitioning]`
|
||||||
|
- `corvu-opening` -\> `&[data-opening]`
|
||||||
|
- `corvu-closing` -\> `&[data-closing]`
|
||||||
|
- `corvu-snapping` -\> `&[data-snapping]`
|
||||||
|
- `corvu-resizing` -\> `&[data-resizing]`
|
||||||
|
- `corvu-disabled` -\> `&[data-disabled]`
|
||||||
|
- `corvu-active` -\> `&[data-active]`
|
||||||
|
- `corvu-dragging` -\> `&[data-dragging]`
|
||||||
|
- `corvu-selected` -\> `&[data-selected]`
|
||||||
|
- `corvu-today` -\> `&[data-today]`
|
||||||
|
- `corvu-range-start` -\> `&[data-range-start]`
|
||||||
|
- `corvu-range-end` -\> `&[data-range-end]`
|
||||||
|
- `corvu-in-range` -\> `&[data-in-range]`
|
||||||
|
- `corvu-side-top` -\> `&[data-side='top']`
|
||||||
|
- `corvu-side-right` -\> `&[data-side='right']`
|
||||||
|
- `corvu-side-bottom` -\> `&[data-side='bottom']`
|
||||||
|
- `corvu-side-left` -\> `&[data-side='left']`
|
||||||
|
|
||||||
|
These two CSS framework use similar syntax. You can style components like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Dialog.Content
|
||||||
|
class="corvu-open:animate-in corvu-closed:animate-out"
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</Dialog.Content>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Animation [Section titled Animation](https://corvu.dev/docs/styling/\#animation)
|
||||||
|
|
||||||
|
corvu has built-in support for CSS animations and waits for any pending animation to finish before removing an element from the DOM. This means you can use CSS animations to animate the appearance and disappearance of primitives. Every unmountable component also provides a `forceMount` property which forces it to stay mounted in the DOM even when it is not visible. This is useful when using third-party animation libraries.
|
||||||
|
|
||||||
|
Developed and designed by [Jasmin](https://github.com/GiyoMoon/)
|
||||||
79
.firecrawl/csszone-anchor.md
Normal file
79
.firecrawl/csszone-anchor.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# CSS Anchor Positioning 2026: Practical Guide for Tooltips, Menus, and Smart Overlays
|
||||||
|
|
||||||
|
Positioning contextual UI has always been painful. Dropdowns, tooltips, and popovers often require JavaScript calculations, viewport checks, and custom collision logic. CSS Anchor Positioning moves a large part of this work into native styling.
|
||||||
|
|
||||||
|
## Why It Matters
|
||||||
|
|
||||||
|
- dropdown clipped on small screens
|
||||||
|
- tooltip detached from trigger on scroll
|
||||||
|
- menu appears off-screen in RTL or localized UI
|
||||||
|
|
||||||
|
## Core Mental Model
|
||||||
|
|
||||||
|
Define an anchor element
|
||||||
|
Attach floating UI to that anchor
|
||||||
|
Let CSS handle alignment behavior
|
||||||
|
|
||||||
|
## Real Use Cases
|
||||||
|
|
||||||
|
- action menu in data tables
|
||||||
|
- profile dropdown in sticky headers
|
||||||
|
- inline help tooltip in forms
|
||||||
|
- contextual edit controls in CMS
|
||||||
|
|
||||||
|
## UX Rules
|
||||||
|
|
||||||
|
- keep overlays close to trigger
|
||||||
|
- preserve keyboard focus flow
|
||||||
|
- add deterministic closing behavior
|
||||||
|
- avoid huge animation while repositioning
|
||||||
|
|
||||||
|
## Rollout Strategy
|
||||||
|
|
||||||
|
tooltip
|
||||||
|
dropdown
|
||||||
|
popover with richer content
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Anchor positioning is a structural improvement that reduces UI fragility in real products.
|
||||||
|
|
||||||
|
## Related posts
|
||||||
|
|
||||||
|
Continue reading on nearby topics.
|
||||||
|
|
||||||
|
[Latest CSS Gradient Features and Trends for 2026Latest CSS gradient features for 2026: new color combinations, mesh techniques, animated transitions, and practical production patterns.](https://css-zone.com/blog/css-gradient-trends-2026) [Core Web Vitals 2026: CSS Playbook for Faster LCP, Better INP, and Stable CLSA practical Core Web Vitals 2026 guide focused on CSS architecture, rendering strategy, font loading, and layout stability for real products.](https://css-zone.com/blog/core-web-vitals-2026-css-playbook) [CSS Best Practices for Real Projects: A Practical Playbook from CSS-Zone.comA practical CSS guide for production teams: architecture, naming, tokens, responsive strategy, performance, and accessibility. Includes many copy-paste-ready examples and workflows used on CSS-Zone.com.](https://css-zone.com/blog/css-best-practices-real-projects-css-zone) [Modern CSS Features You Should Use in 2026Explore the latest CSS features that are changing web development: container queries, :has() selector, cascade layers, and more cutting-edge techniques.](https://css-zone.com/blog/modern-css-features-2026)
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
0
|
||||||
|
|
||||||
|
Sign in to leave a comment.
|
||||||
|
|
||||||
|
Sign in
|
||||||
|
|
||||||
|
No comments yet. Be the first.
|
||||||
|
|
||||||
|
Cookies
|
||||||
|
|
||||||
|
## We use cookies to keep things smooth
|
||||||
|
|
||||||
|
They help keep you signed in, remember your preferences, and measure what features land. You control what happens next.
|
||||||
|
|
||||||
|
[Privacy Policy](https://css-zone.com/privacy-policy) [Cookie rules](https://css-zone.com/cookie-policy)
|
||||||
|
|
||||||
|
Not nowAccept cookies
|
||||||
|
|
||||||
|
Contact
|
||||||
|
|
||||||
|
CSS file\_type\_scss
|
||||||
|
|
||||||
|
reCAPTCHA
|
||||||
|
|
||||||
|
Recaptcha requires verification.
|
||||||
|
|
||||||
|
[Privacy](https://www.google.com/intl/en/policies/privacy/) \- [Terms](https://www.google.com/intl/en/policies/terms/)
|
||||||
|
|
||||||
|
protected by **reCAPTCHA**
|
||||||
|
|
||||||
|
[Privacy](https://www.google.com/intl/en/policies/privacy/) \- [Terms](https://www.google.com/intl/en/policies/terms/)
|
||||||
382
.firecrawl/devproportal-radix-react-aria.md
Normal file
382
.firecrawl/devproportal-radix-react-aria.md
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
[↓\\
|
||||||
|
Skip to main content](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/#main-content)
|
||||||
|
|
||||||
|
[DevPro Portal](https://devproportal.com/)
|
||||||
|
|
||||||
|
Table of Contents
|
||||||
|
|
||||||
|
|
||||||
|
Table of Contents
|
||||||
|
|
||||||
|
|
||||||
|
It’s 2026. If you are still wrestling with `!important` overrides in Material UI or trying to hack the internal DOM structure of a Bootstrap component just to match a Figma design, you’re doing it the hard way.
|
||||||
|
|
||||||
|
The React ecosystem has matured. We’ve moved past the era of “All-in-One” component kits that dictate your styling. We are firmly in the era of **Headless UI**.
|
||||||
|
|
||||||
|
As senior developers and architects, our goal isn’t just to put pixels on the screen; it’s to ship accessible, robust, and performant interfaces that scale. We want full control over the CSS (likely via Tailwind or CSS-in-JS) without reinventing the complex logic required for keyboard navigation, focus management, and screen reader support.
|
||||||
|
|
||||||
|
This article dives deep into the two heavyweights of the headless world: **Radix UI** and **React Aria**. We’ll compare them, build real components, and discuss the architectural implications of choosing one over the other.
|
||||||
|
|
||||||
|
## The Headless Architecture [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#the-headless-architecture)
|
||||||
|
|
||||||
|
Before we touch the code, let’s align on the mental model. “Headless” doesn’t mean “no UI”; it means “unopinionated UI”.
|
||||||
|
|
||||||
|
In a traditional library (like AntD or MUI), the logic and the styling are coupled. In a Headless library, the library provides the **behavior** and **state**, while you provide the **rendering** and **styling**.
|
||||||
|
|
||||||
|
Here is how the data flow looks in a modern Headless setup:
|
||||||
|
|
||||||
|
Your Code
|
||||||
|
|
||||||
|
Headless Layer (Radix/Aria)
|
||||||
|
|
||||||
|
User Interaction
|
||||||
|
|
||||||
|
State Management
|
||||||
|
|
||||||
|
WAI-ARIA Roles
|
||||||
|
|
||||||
|
Focus Trap / Loops
|
||||||
|
|
||||||
|
Keyboard/Mouse Events
|
||||||
|
|
||||||
|
Tailwind / CSS Modules
|
||||||
|
|
||||||
|
JSX Structure
|
||||||
|
|
||||||
|
Framer Motion
|
||||||
|
|
||||||
|
Rendered DOM
|
||||||
|
|
||||||
|
### Why does this matter? [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#why-does-this-matter)
|
||||||
|
|
||||||
|
1. **Accessibility (a11y) is hard:** Implementing a fully accessible Dropdown Menu takes weeks of testing across VoiceOver, NVDA, and JAWS. Headless libraries give you this for free.
|
||||||
|
2. **Design Freedom:** You own the `className`.
|
||||||
|
3. **Bundle Size:** You only import the logic you need.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Prerequisites and Environment [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#prerequisites-and-environment)
|
||||||
|
|
||||||
|
To follow along, ensure you have a modern React environment set up. We are assuming a 2026 standard stack:
|
||||||
|
|
||||||
|
- **Node.js:** v20+ (LTS)
|
||||||
|
- **React:** v19
|
||||||
|
- **Styling:** Tailwind CSS v4 (or v3.4+)
|
||||||
|
- **Icons:** Lucide React (optional but recommended)
|
||||||
|
|
||||||
|
### Setup [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#setup)
|
||||||
|
|
||||||
|
We’ll create a lightweight sandbox.
|
||||||
|
|
||||||
|
Copy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a Vite project
|
||||||
|
npm create vite@latest headless-demo -- --template react-ts
|
||||||
|
|
||||||
|
# Enter directory
|
||||||
|
cd headless-demo
|
||||||
|
|
||||||
|
# Install dependencies (We will use both for comparison)
|
||||||
|
npm install @radix-ui/react-popover @radix-ui/react-dialog react-aria-components class-variance-authority clsx tailwind-merge framer-motion
|
||||||
|
|
||||||
|
# Start dev server
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
_Note: We included `class-variance-authority` (CVA) and `tailwind-merge`. These are the bread and butter for handling styles in headless components._
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## The Contenders: Radix vs. React Aria [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#the-contenders-radix-vs-react-aria)
|
||||||
|
|
||||||
|
Choosing between these two is often the first architectural decision when building a Design System.
|
||||||
|
|
||||||
|
| Feature | Radix UI | React Aria (Adobe) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Philosophy** | Component-first. Provides unstyled primitives (e.g., `<Dialog.Root>`). | Hooks-first (historically), now offers Components. “Industrial Grade” a11y. |
|
||||||
|
| **API Surface** | simpler, cleaner JSX. Very “React-y”. | Extremely granular. Offers `useButton`, `useSelect`, etc., plus a new Component API. |
|
||||||
|
| **Styling** | Agnostic. Works perfectly with Tailwind. | Agnostic. The new `react-aria-components` has a specific `className` function for states (hover/focus). |
|
||||||
|
| **Animation** | Relies on external libs or CSS keyframes. Works great with Framer Motion. | Has built-in animation support in the new components API, but generally external. |
|
||||||
|
| **Mobile** | Good, but sometimes lacks nuanced touch interactions. | Best in class. Adobe tests on everything. Handles virtual keyboard quirks brilliantly. |
|
||||||
|
| **Bundle Size** | Modular. You install packages individually (e.g., `@radix-ui/react-tooltip`). | Modular, but the core logic is heavier due to extreme robustness. |
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Part 1: Building with Radix UI [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#part-1-building-with-radix-ui)
|
||||||
|
|
||||||
|
Radix is generally the favorite for teams that want to move fast but maintain high quality. It powers the popular `shadcn/ui` collection.
|
||||||
|
|
||||||
|
Let’s build a **Popover** component. This isn’t just a tooltip; it needs to handle focus trapping (optional), closing on outside clicks, and keyboard formatting.
|
||||||
|
|
||||||
|
### The Implementation [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#the-implementation)
|
||||||
|
|
||||||
|
We will use Tailwind for styling and `lucide-react` for an icon.
|
||||||
|
|
||||||
|
Copy
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/components/RadixPopover.tsx
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as Popover from '@radix-ui/react-popover';
|
||||||
|
import { Settings2, X } from 'lucide-react';
|
||||||
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
// Utility for cleaner classes
|
||||||
|
function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserSettingsPopover() {
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center rounded-full w-10 h-10",
|
||||||
|
"bg-slate-100 text-slate-900 hover:bg-slate-200",
|
||||||
|
"focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2",
|
||||||
|
"transition-colors duration-200"
|
||||||
|
)}
|
||||||
|
aria-label="Update dimensions"
|
||||||
|
>
|
||||||
|
<Settings2 size={20} />
|
||||||
|
</button>
|
||||||
|
</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Portal>
|
||||||
|
<Popover.Content
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg p-5 w-[260px] bg-white shadow-[0_10px_38px_-10px_rgba(22,23,24,0.35),0_10px_20px_-15px_rgba(22,23,24,0.2)]",
|
||||||
|
"border border-slate-200",
|
||||||
|
"will-change-[transform,opacity]",
|
||||||
|
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
|
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||||
|
"side-top:slide-in-from-bottom-2 side-bottom:slide-in-from-top-2"
|
||||||
|
)}
|
||||||
|
sideOffset={5}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2.5">
|
||||||
|
<p className="text-sm font-medium text-slate-900 mb-2">Dimensions</p>
|
||||||
|
|
||||||
|
<fieldset className="flex items-center gap-5">
|
||||||
|
<label className="text-xs text-slate-500 w-[75px]" htmlFor="width">Width</label>
|
||||||
|
<input
|
||||||
|
className="w-full h-8 px-2 text-xs border rounded text-slate-700 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||||
|
id="width"
|
||||||
|
defaultValue="100%"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset className="flex items-center gap-5">
|
||||||
|
<label className="text-xs text-slate-500 w-[75px]" htmlFor="height">Height</label>
|
||||||
|
<input
|
||||||
|
className="w-full h-8 px-2 text-xs border rounded text-slate-700 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||||
|
id="height"
|
||||||
|
defaultValue="25px"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover.Close asChild>
|
||||||
|
<button
|
||||||
|
className="absolute top-3 right-3 w-6 h-6 inline-flex items-center justify-center rounded-full text-slate-500 hover:bg-slate-100 focus:shadow-[0_0_0_2px] focus:shadow-slate-400 outline-none"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</Popover.Close>
|
||||||
|
|
||||||
|
<Popover.Arrow className="fill-white" />
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analysis [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#analysis)
|
||||||
|
|
||||||
|
1. **`asChild` Pattern:** Notice `<Popover.Trigger asChild>`. This is Radix’s signature move. It merges the event handlers and ARIA attributes onto _your_ DOM node (`<button>`) instead of wrapping it in an unnecessary `<div>`.
|
||||||
|
2. **The Portal:**`<Popover.Portal>` automatically transports the content to `document.body`. This solves the classic `z-index` and `overflow: hidden` parent clipping issues.
|
||||||
|
3. **Data Attributes:** Styling is driven by `data-[state=open]`. This allows us to use pure CSS (or Tailwind utilities) for entry/exit animations without complex React state logic.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Part 2: Building with React Aria Components (RAC) [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#part-2-building-with-react-aria-components-rac)
|
||||||
|
|
||||||
|
React Aria used to be known for its low-level hooks (`useButton`, `useOverlay`), which were powerful but verbose. In late 2024/2026, the standard is **React Aria Components (RAC)**. These provide a component-based API similar to Radix but with Adobe’s rigorous “interaction modeling.”
|
||||||
|
|
||||||
|
Let’s build a **Select (Dropdown)**. HTML `<select>` is notoriously hard to style.
|
||||||
|
|
||||||
|
### The Implementation [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#the-implementation-1)
|
||||||
|
|
||||||
|
Copy
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/components/AriaSelect.tsx
|
||||||
|
import type { Key } from 'react-aria-components';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
ListBox,
|
||||||
|
ListBoxItem,
|
||||||
|
Popover,
|
||||||
|
Select,
|
||||||
|
SelectValue
|
||||||
|
} from 'react-aria-components';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function AriaSelect() {
|
||||||
|
return (
|
||||||
|
<Select className="flex flex-col gap-1 w-[200px]">
|
||||||
|
<Label className="text-sm font-medium text-slate-700 ml-1">Favorite Framework</Label>
|
||||||
|
|
||||||
|
<Button className={({ isPressed, isFocusVisible }) => `
|
||||||
|
flex items-center justify-between w-full px-3 py-2
|
||||||
|
bg-white border rounded-lg shadow-sm text-left cursor-default
|
||||||
|
${isFocusVisible ? 'ring-2 ring-blue-500 border-blue-500 outline-none' : 'border-slate-300'}
|
||||||
|
${isPressed ? 'bg-slate-50' : ''}
|
||||||
|
`}>
|
||||||
|
<SelectValue className="text-sm text-slate-900 placeholder-shown:text-slate-400" />
|
||||||
|
<ChevronDown size={16} className="text-slate-500" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Popover className={({ isEntering, isExiting }) => `
|
||||||
|
overflow-auto rounded-lg drop-shadow-lg border border-slate-200 bg-white
|
||||||
|
w-[var(--trigger-width)]
|
||||||
|
${isEntering ? 'animate-in fade-in zoom-in-95 duration-200' : ''}
|
||||||
|
${isExiting ? 'animate-out fade-out zoom-out-95 duration-200' : ''}
|
||||||
|
`}>
|
||||||
|
<ListBox className="p-1 outline-none">
|
||||||
|
{['React', 'Vue', 'Svelte', 'Angular', 'Qwik'].map((item) => (
|
||||||
|
<ListBoxItem
|
||||||
|
key={item}
|
||||||
|
id={item}
|
||||||
|
textValue={item}
|
||||||
|
className={({ isFocused, isSelected }) => `
|
||||||
|
cursor-default select-none rounded px-2 py-1.5 text-sm outline-none
|
||||||
|
${isFocused ? 'bg-blue-100 text-blue-900' : 'text-slate-700'}
|
||||||
|
${isSelected ? 'font-semibold' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{({ isSelected }) => (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>{item}</span>
|
||||||
|
{isSelected && <span>✓</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ListBoxItem>
|
||||||
|
))}
|
||||||
|
</ListBox>
|
||||||
|
</Popover>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analysis [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#analysis-1)
|
||||||
|
|
||||||
|
1. **Render Props for Styling:** RAC uses render props heavily for styling classes (e.g., `className={({ isFocused }) => ...}`). This exposes the internal interaction state directly to Tailwind. You don’t need `data-` attributes; you have direct JS boolean access.
|
||||||
|
2. **Adaptive Behavior:** Adobe put incredible effort into mobile. On mobile devices, this component handles touch cancellation, scrolling behavior, and virtual keyboard avoidance better than almost any other library.
|
||||||
|
3. **Semantics:** The `<ListBox>` and `<ListBoxItem>` components ensure correct ARIA roles (`role="listbox"`, `role="option"`) are applied, which are different from a standard navigation menu.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Performance and Common Pitfalls [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#performance-and-common-pitfalls)
|
||||||
|
|
||||||
|
When implementing Headless UI in a production environment, watch out for these traps.
|
||||||
|
|
||||||
|
### 1\. The Bundle Size Myth [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#1-the-bundle-size-myth)
|
||||||
|
|
||||||
|
You might think, “I’m importing a huge library!” Not really. Both libraries support tree-shaking.
|
||||||
|
|
||||||
|
- **Radix:** You usually install specific packages (`@radix-ui/react-dialog`).
|
||||||
|
- **React Aria:** The monolithic package exports everything, but modern bundlers (Vite/Rollup/Webpack 5) shake out unused exports effectively.
|
||||||
|
|
||||||
|
However, React Aria IS logically heavier because it includes code for edge cases you didn’t know existed (like specific screen reader bugs in older iOS versions).
|
||||||
|
|
||||||
|
### 2\. Focus Management [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#2-focus-management)
|
||||||
|
|
||||||
|
The number one bug in custom modals is **Focus Trapping**.
|
||||||
|
|
||||||
|
- **Scenario:** User opens a modal. User hits `Tab`. Focus goes _behind_ the modal to the URL bar or the background content.
|
||||||
|
- **Solution:** Both libraries handle this, but you must ensure you don’t accidentally unmount the component before the closing animation finishes. Radix handles this with `data-state` and animations, but manual conditional rendering (`{isOpen && <Dialog />}`) without `AnimatePresence` (if using Framer) can break the focus return feature.
|
||||||
|
|
||||||
|
### 3\. Z-Index Wars [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#3-z-index-wars)
|
||||||
|
|
||||||
|
Both libraries use **Portals**.
|
||||||
|
|
||||||
|
- **Trap:** If your global CSS sets a high z-index on a sticky header, your ported modal might end up under it if the portal container isn’t managed correctly.
|
||||||
|
- **Best Practice:** Create a dedicated stacking context or ensure your portal root (usually `body`) is handled correctly in your Tailwind config (`z-50` usually suffices for modals).
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Conclusion: Which one should you choose? [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#conclusion-which-one-should-you-choose)
|
||||||
|
|
||||||
|
As an architect, your choice depends on your team’s priorities.
|
||||||
|
|
||||||
|
**Choose Radix UI if:**
|
||||||
|
|
||||||
|
- You want a developer experience that feels “native” to React.
|
||||||
|
- You are heavily invested in the Tailwind ecosystem (it pairs beautifully).
|
||||||
|
- You need to ship fast and “Very Good” accessibility is acceptable.
|
||||||
|
- You are building a standard B2B SaaS dashboard.
|
||||||
|
|
||||||
|
**Choose React Aria if:**
|
||||||
|
|
||||||
|
- **Accessibility is non-negotiable.** (e.g., Government, Healthcare, Education).
|
||||||
|
- You need robust touch/mobile interactions (drag and drop, swipes).
|
||||||
|
- You prefer render-props for styling logic over CSS selectors.
|
||||||
|
- You are building a complex design system that needs to last 5+ years.
|
||||||
|
|
||||||
|
Both libraries represent the pinnacle of React component development in 2026. By separating behavior from design, they allow us to build UIs that are unique to our brand but universal in their usability.
|
||||||
|
|
||||||
|
### Further Reading [\#](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/\#further-reading)
|
||||||
|
|
||||||
|
- [WAI-ARIA Authoring Practices Guide (APG)](https://www.w3.org/WAI/ARIA/apg/) \- The bible for accessible patterns.
|
||||||
|
- [Radix UI Docs](https://www.radix-ui.com/)
|
||||||
|
- [React Aria Components Docs](https://react-spectrum.adobe.com/react-aria/components.html)
|
||||||
|
|
||||||
|
**Stop reinventing the wheel. Import the wheel, and paint it whatever color you want.**
|
||||||
|
|
||||||
|
## Related Articles
|
||||||
|
|
||||||
|
- · [React Compiler: The End of Manual Memoization and the Era of Auto-Optimization](https://devproportal.com/frontend/react/react-compiler-eliminates-memoization/)
|
||||||
|
- · [Mastering Concurrent Rendering: A Deep Dive into Transitions and Deferring](https://devproportal.com/frontend/react/mastering-react-concurrent-mode-transitions-deferring/)
|
||||||
|
- · [React 19 Deep Dive: Mastering the Compiler, Actions, and Advanced Hooks](https://devproportal.com/frontend/react/react-19-deep-dive-compiler-actions-hooks/)
|
||||||
|
- · [State of React State: Redux Toolkit vs. Zustand vs. Signals](https://devproportal.com/frontend/react/react-state-management-showdown-redux-zustand-signals/)
|
||||||
|
- · [React 19 Architecture: Mastering the Server vs. Client Component Paradigm](https://devproportal.com/frontend/react/react-19-server-vs-client-components-guide/)
|
||||||
|
|
||||||
|
## The Architect’s Pulse: Engineering Intelligence
|
||||||
|
|
||||||
|
As a CTO with 21+ years of experience, I deconstruct the complexities of high-performance backends. Join our technical circle to receive weekly strategic drills on JVM internals, Go concurrency, and cloud-native resilience. No fluff, just pure architectural execution.
|
||||||
|
|
||||||
|
Which technical challenge are you currently deconstructing?
|
||||||
|
|
||||||
|
Option A: Master patterns in Go, Java, and Node.js.
|
||||||
|
|
||||||
|
Option B: Deep-dive into Database internals and Sharding.
|
||||||
|
|
||||||
|
Option C: Orchestrating resilience with K8s and Microservices.
|
||||||
|
|
||||||
|
Option D: Deconstructing real-world architectural case studies.
|
||||||
|
|
||||||
|
Join the Inner Circle
|
||||||
|
|
||||||
|
[Built with Kit](https://kit.com/features/forms?utm_campaign=poweredby&utm_content=form&utm_medium=referral&utm_source=dynamic)
|
||||||
|
|
||||||
|
[↑](https://devproportal.com/frontend/react/mastering-headless-ui-radix-vs-react-aria/#the-top "Scroll to top")
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
[](https://devproportal.com/)
|
||||||
|
|
||||||
|
© 2026 DevPro Portal. All rights reserved.
|
||||||
|
|
||||||
|
|
||||||
|
Powered by [Stonehenge EdTech](https://www.stonehengeedtech.com/).
|
||||||
|
|
||||||
|
|
||||||
|
[About Us](https://devproportal.com/about-us) [Terms of Service](https://devproportal.com/terms-of-service) [Privacy Policy](https://devproportal.com/privacy-policy) [Cookie Policy](https://devproportal.com/cookie-policy) [Support & Contact](https://devproportal.com/contact)
|
||||||
224
.firecrawl/dewidar-headless-patterns.md
Normal file
224
.firecrawl/dewidar-headless-patterns.md
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
[Back to Blog](https://mohameddewidar.com/blog)
|
||||||
|
|
||||||
|
# Headless UI Patterns in React
|
||||||
|
|
||||||
|
Headless UI is a pattern that separates **logic** from **presentation**. The component controls state and behavior while leaving all markup and styling to the consumer. This pattern is used in modern design systems and UI libraries because it gives developers full visual freedom without sacrificing predictable logic.
|
||||||
|
|
||||||
|
This article explains the pattern clearly, shows how to implement it in React, and provides real world examples that scale in production systems.
|
||||||
|
|
||||||
|
## 1\. What Headless UI Means
|
||||||
|
|
||||||
|
A headless component does not render its own layout. It exposes:
|
||||||
|
|
||||||
|
- state
|
||||||
|
- actions
|
||||||
|
- event handlers
|
||||||
|
- accessibility helpers
|
||||||
|
|
||||||
|
The consumer decides how the UI looks.
|
||||||
|
|
||||||
|
Example of a headless toggle hook:
|
||||||
|
|
||||||
|
```
|
||||||
|
function useToggle(initial = false) {
|
||||||
|
const [on, setOn] = useState(initial);
|
||||||
|
return {
|
||||||
|
on,
|
||||||
|
toggle: () => setOn((v) => !v),
|
||||||
|
setOn
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage with custom UI:
|
||||||
|
|
||||||
|
```
|
||||||
|
const toggle = useToggle();
|
||||||
|
|
||||||
|
<button onClick={toggle.toggle}>
|
||||||
|
{toggle.on ? "On" : "Off"}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
The hook manages behavior. The UI is entirely customizable.
|
||||||
|
|
||||||
|
## 2\. Why Headless UI Matters
|
||||||
|
|
||||||
|
Headless components solve real problems:
|
||||||
|
|
||||||
|
### Problem 1. Components that restrict styling
|
||||||
|
|
||||||
|
Teams often fight against rigid components that force markup or CSS structures.
|
||||||
|
|
||||||
|
### Problem 2. Hard to integrate with custom design systems
|
||||||
|
|
||||||
|
Headless logic works with any HTML structure and any design system.
|
||||||
|
|
||||||
|
### Problem 3. Difficult to reuse logic without duplicating UI
|
||||||
|
|
||||||
|
The logic lives in one place. The UI can vary by use case.
|
||||||
|
|
||||||
|
### Problem 4. Mixed concerns
|
||||||
|
|
||||||
|
UI, behavior, and state are combined in one file. Headless UI splits these into clear boundaries.
|
||||||
|
|
||||||
|
## 3\. Headless UI With Render Props
|
||||||
|
|
||||||
|
Render props expose behavior through a function.
|
||||||
|
|
||||||
|
```
|
||||||
|
function Dropdown({ children }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return children({
|
||||||
|
open,
|
||||||
|
toggle: () => setOpen((v) => !v)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Dropdown>
|
||||||
|
{({ open, toggle }) => (
|
||||||
|
<>
|
||||||
|
<button onClick={toggle}>Menu</button>
|
||||||
|
{open && <div className="menu">...</div>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives complete visual control.
|
||||||
|
|
||||||
|
## 4\. Headless UI With Context and Compound Components
|
||||||
|
|
||||||
|
This pattern is cleaner for multi part components like modals, dropdowns, and accordions.
|
||||||
|
|
||||||
|
### Step 1. Create context driven logic
|
||||||
|
|
||||||
|
```
|
||||||
|
const AccordionContext = createContext(null);
|
||||||
|
|
||||||
|
function Accordion({ children }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
open,
|
||||||
|
toggle: () => setOpen((v) => !v)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AccordionContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2. Separate UI components
|
||||||
|
|
||||||
|
```
|
||||||
|
function AccordionTrigger({ children }) {
|
||||||
|
const { toggle } = useContext(AccordionContext);
|
||||||
|
return <button onClick={toggle}>{children}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({ children }) {
|
||||||
|
const { open } = useContext(AccordionContext);
|
||||||
|
return open ? <div>{children}</div> : null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Accordion>
|
||||||
|
<AccordionTrigger>Show Details</AccordionTrigger>
|
||||||
|
<AccordionContent>Here are the details...</AccordionContent>
|
||||||
|
</Accordion>
|
||||||
|
```
|
||||||
|
|
||||||
|
The logic is shared. The UI remains flexible.
|
||||||
|
|
||||||
|
## 5\. Building a Headless Select Component
|
||||||
|
|
||||||
|
Logic:
|
||||||
|
|
||||||
|
```
|
||||||
|
function useSelect(items) {
|
||||||
|
const [selected, setSelected] = useState(null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
selected,
|
||||||
|
select: (item) => setSelected(item)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
const select = useSelect(["A", "B", "C"]);
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{select.items.map((item) => (
|
||||||
|
<li key={item} onClick={() => select.select(item)}>
|
||||||
|
{item} {select.selected === item ? "(selected)" : ""}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6\. Accessibility in Headless UI
|
||||||
|
|
||||||
|
A headless design system should expose accessibility helpers.
|
||||||
|
|
||||||
|
```
|
||||||
|
function useDialog() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
open,
|
||||||
|
triggerProps: {
|
||||||
|
"aria-haspopup": "dialog",
|
||||||
|
onClick: () => setOpen(true)
|
||||||
|
},
|
||||||
|
dialogProps: {
|
||||||
|
role: "dialog",
|
||||||
|
"aria-modal": true
|
||||||
|
},
|
||||||
|
close: () => setOpen(false)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
<button {...dialog.triggerProps}>Open</button>
|
||||||
|
|
||||||
|
{dialog.open && (
|
||||||
|
<div {...dialog.dialogProps}>
|
||||||
|
Modal content
|
||||||
|
<button onClick={dialog.close}>Close</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7\. Anti Patterns to Avoid
|
||||||
|
|
||||||
|
- Forcing DOM structure
|
||||||
|
- Mixing styling inside the logic
|
||||||
|
- Bloated configuration props
|
||||||
|
- Hooks that try to manage both logic and rendering
|
||||||
|
|
||||||
|
## Final Thoughts
|
||||||
|
|
||||||
|
Headless UI patterns help teams build flexible and reusable components that scale with evolving design requirements. By separating logic from presentation, React applications become easier to maintain, more customizable, and more predictable.
|
||||||
|
|
||||||
|
[Back to Blog](https://mohameddewidar.com/blog)
|
||||||
597
.firecrawl/json-render.md
Normal file
597
.firecrawl/json-render.md
Normal file
@ -0,0 +1,597 @@
|
|||||||
|
The Generative UI Framework
|
||||||
|
|
||||||
|
# AI → json-render → UI
|
||||||
|
|
||||||
|
Generate dynamic, personalized UIs from prompts without sacrificing reliability. Predefined components and actions for safe, predictable output.
|
||||||
|
|
||||||
|
Create a contact form with name, email, and message
|
||||||
|
|
||||||
|
Create a login form with email and passwordBuild a feedback form with rating stars
|
||||||
|
|
||||||
|
jsonnestedstreamcatalog
|
||||||
|
|
||||||
|
```
|
||||||
|
{"op":"add","path":"/root","value":"card"}
|
||||||
|
{"op":"add","path":"/elements/name","value":{"type":"Input","props":{"label":"Name","name":"name","statePath":"/form/name","checks":[{"type":"required","message":"Name is required"}]}}}
|
||||||
|
{"op":"add","path":"/elements/email","value":{"type":"Input","props":{"label":"Email","name":"email","type":"email","statePath":"/form/email","checks":[{"type":"required","message":"Email is required"},{"type":"email","message":"Please enter a valid email"}]}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"root": "card",
|
||||||
|
"state": {
|
||||||
|
"form": {
|
||||||
|
"name": "",
|
||||||
|
"email": "",
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"elements": {
|
||||||
|
"card": {
|
||||||
|
"type": "Card",
|
||||||
|
"props": {
|
||||||
|
"title": "Contact Us",
|
||||||
|
"maxWidth": "md"
|
||||||
|
},
|
||||||
|
"children": [\
|
||||||
|
"name",\
|
||||||
|
"email"\
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "Input",
|
||||||
|
"props": {
|
||||||
|
"label": "Name",
|
||||||
|
"name": "name",
|
||||||
|
"statePath": "/form/name",
|
||||||
|
"checks": [\
|
||||||
|
{\
|
||||||
|
"type": "required",\
|
||||||
|
"message": "Name is required"\
|
||||||
|
}\
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "Input",
|
||||||
|
"props": {
|
||||||
|
"label": "Email",
|
||||||
|
"name": "email",
|
||||||
|
"type": "email",
|
||||||
|
"statePath": "/form/email",
|
||||||
|
"checks": [\
|
||||||
|
{\
|
||||||
|
"type": "required",\
|
||||||
|
"message": "Email is required"\
|
||||||
|
},\
|
||||||
|
{\
|
||||||
|
"type": "email",\
|
||||||
|
"message": "Please enter a valid email"\
|
||||||
|
}\
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"state": {
|
||||||
|
"form": {
|
||||||
|
"name": "",
|
||||||
|
"email": "",
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"elements": {
|
||||||
|
"type": "Card",
|
||||||
|
"props": {
|
||||||
|
"title": "Contact Us",
|
||||||
|
"maxWidth": "md"
|
||||||
|
},
|
||||||
|
"children": [\
|
||||||
|
{\
|
||||||
|
"type": "Input",\
|
||||||
|
"props": {\
|
||||||
|
"label": "Name",\
|
||||||
|
"name": "name",\
|
||||||
|
"statePath": "/form/name",\
|
||||||
|
"checks": [\
|
||||||
|
{\
|
||||||
|
"type": "required",\
|
||||||
|
"message": "Name is required"\
|
||||||
|
}\
|
||||||
|
]\
|
||||||
|
}\
|
||||||
|
},\
|
||||||
|
{\
|
||||||
|
"type": "Input",\
|
||||||
|
"props": {\
|
||||||
|
"label": "Email",\
|
||||||
|
"name": "email",\
|
||||||
|
"type": "email",\
|
||||||
|
"statePath": "/form/email",\
|
||||||
|
"checks": [\
|
||||||
|
{\
|
||||||
|
"type": "required",\
|
||||||
|
"message": "Email is required"\
|
||||||
|
},\
|
||||||
|
{\
|
||||||
|
"type": "email",\
|
||||||
|
"message": "Please enter a valid email"\
|
||||||
|
}\
|
||||||
|
]\
|
||||||
|
}\
|
||||||
|
}\
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
components (39)actions (6)
|
||||||
|
|
||||||
|
Accordion
|
||||||
|
|
||||||
|
Collapsible sections. Items as \[{title, content}\]. Type 'single' (default) or 'multiple'.
|
||||||
|
|
||||||
|
items: arraytype: enum?
|
||||||
|
|
||||||
|
Alert
|
||||||
|
|
||||||
|
Alert banner
|
||||||
|
|
||||||
|
title: stringmessage: string?type: enum?
|
||||||
|
|
||||||
|
Avatar
|
||||||
|
|
||||||
|
User avatar with fallback initials
|
||||||
|
|
||||||
|
src: string?name: stringsize: enum?
|
||||||
|
|
||||||
|
Badge
|
||||||
|
|
||||||
|
Status badge
|
||||||
|
|
||||||
|
text: stringvariant: enum?
|
||||||
|
|
||||||
|
BarGraph
|
||||||
|
|
||||||
|
Vertical bar chart
|
||||||
|
|
||||||
|
title: string?data: array
|
||||||
|
|
||||||
|
Button
|
||||||
|
|
||||||
|
Clickable button. Bind on.press for handler.
|
||||||
|
|
||||||
|
label: stringvariant: enum?disabled: boolean?
|
||||||
|
|
||||||
|
on.press
|
||||||
|
|
||||||
|
ButtonGroup
|
||||||
|
|
||||||
|
Segmented button group. Use { $bindState } on selected for selected value.
|
||||||
|
|
||||||
|
buttons: arrayselected: string?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
Cardslots: default
|
||||||
|
|
||||||
|
Container card for content sections. Use for forms/content boxes, NOT for page headers.
|
||||||
|
|
||||||
|
title: string?description: string?maxWidth: enum?centered: boolean?
|
||||||
|
|
||||||
|
Carousel
|
||||||
|
|
||||||
|
Horizontally scrollable carousel of cards.
|
||||||
|
|
||||||
|
items: array
|
||||||
|
|
||||||
|
Checkbox
|
||||||
|
|
||||||
|
Checkbox input. Use { $bindState } on checked for binding.
|
||||||
|
|
||||||
|
label: stringname: stringchecked: boolean?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
Collapsibleslots: default
|
||||||
|
|
||||||
|
Collapsible section with trigger. Children render inside.
|
||||||
|
|
||||||
|
title: stringdefaultOpen: boolean?
|
||||||
|
|
||||||
|
Dialogslots: default
|
||||||
|
|
||||||
|
Modal dialog. Set openPath to a boolean state path. Use setState to toggle.
|
||||||
|
|
||||||
|
title: stringdescription: string?openPath: string
|
||||||
|
|
||||||
|
Drawerslots: default
|
||||||
|
|
||||||
|
Bottom sheet drawer. Set openPath to a boolean state path. Use setState to toggle.
|
||||||
|
|
||||||
|
title: stringdescription: string?openPath: string
|
||||||
|
|
||||||
|
DropdownMenu
|
||||||
|
|
||||||
|
Dropdown menu with trigger button and selectable items.
|
||||||
|
|
||||||
|
label: stringitems: array
|
||||||
|
|
||||||
|
on.select
|
||||||
|
|
||||||
|
Gridslots: default
|
||||||
|
|
||||||
|
Grid layout (1-6 columns)
|
||||||
|
|
||||||
|
columns: number?gap: enum?
|
||||||
|
|
||||||
|
Heading
|
||||||
|
|
||||||
|
Heading text (h1-h4)
|
||||||
|
|
||||||
|
text: stringlevel: enum?
|
||||||
|
|
||||||
|
Image
|
||||||
|
|
||||||
|
Placeholder image (displays alt text in a styled box)
|
||||||
|
|
||||||
|
alt: stringwidth: number?height: number?
|
||||||
|
|
||||||
|
Input
|
||||||
|
|
||||||
|
Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength).
|
||||||
|
|
||||||
|
label: stringname: stringtype: enum?placeholder: string?value: string?checks: array?
|
||||||
|
|
||||||
|
on.submiton.focuson.blur
|
||||||
|
|
||||||
|
LineGraph
|
||||||
|
|
||||||
|
Line chart with points
|
||||||
|
|
||||||
|
title: string?data: array
|
||||||
|
|
||||||
|
Link
|
||||||
|
|
||||||
|
Anchor link. Bind on.press for click handler.
|
||||||
|
|
||||||
|
label: stringhref: string
|
||||||
|
|
||||||
|
on.press
|
||||||
|
|
||||||
|
Pagination
|
||||||
|
|
||||||
|
Page navigation. Use { $bindState } on page for current page number.
|
||||||
|
|
||||||
|
totalPages: numberpage: number?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
Popover
|
||||||
|
|
||||||
|
Popover that appears on click of trigger.
|
||||||
|
|
||||||
|
trigger: stringcontent: string
|
||||||
|
|
||||||
|
Progress
|
||||||
|
|
||||||
|
Progress bar (value 0-100)
|
||||||
|
|
||||||
|
value: numbermax: number?label: string?
|
||||||
|
|
||||||
|
Radio
|
||||||
|
|
||||||
|
Radio button group. Use { $bindState } on value for binding.
|
||||||
|
|
||||||
|
label: stringname: stringoptions: arrayvalue: string?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
Rating
|
||||||
|
|
||||||
|
Star rating display
|
||||||
|
|
||||||
|
value: numbermax: number?label: string?
|
||||||
|
|
||||||
|
Select
|
||||||
|
|
||||||
|
Dropdown select input. Use { $bindState } on value for binding. Use checks for validation.
|
||||||
|
|
||||||
|
label: stringname: stringoptions: arrayplaceholder: string?value: string?checks: array?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
Separator
|
||||||
|
|
||||||
|
Visual separator line
|
||||||
|
|
||||||
|
orientation: enum?
|
||||||
|
|
||||||
|
Skeleton
|
||||||
|
|
||||||
|
Loading placeholder skeleton
|
||||||
|
|
||||||
|
width: string?height: string?rounded: boolean?
|
||||||
|
|
||||||
|
Slider
|
||||||
|
|
||||||
|
Range slider input. Use { $bindState } on value for binding.
|
||||||
|
|
||||||
|
label: string?min: number?max: number?step: number?value: number?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
Spinner
|
||||||
|
|
||||||
|
Loading spinner indicator
|
||||||
|
|
||||||
|
size: enum?label: string?
|
||||||
|
|
||||||
|
Stackslots: default
|
||||||
|
|
||||||
|
Flex container for layouts
|
||||||
|
|
||||||
|
direction: enum?gap: enum?align: enum?justify: enum?
|
||||||
|
|
||||||
|
Switch
|
||||||
|
|
||||||
|
Toggle switch. Use { $bindState } on checked for binding.
|
||||||
|
|
||||||
|
label: stringname: stringchecked: boolean?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
Table
|
||||||
|
|
||||||
|
Data table. columns: header labels. rows: 2D array of cell strings, e.g. \[\["Alice","admin"\],\["Bob","user"\]\].
|
||||||
|
|
||||||
|
columns: arrayrows: arraycaption: string?
|
||||||
|
|
||||||
|
Tabs
|
||||||
|
|
||||||
|
Tab navigation. Use { $bindState } on value for active tab binding.
|
||||||
|
|
||||||
|
tabs: arraydefaultValue: string?value: string?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
Text
|
||||||
|
|
||||||
|
Paragraph text
|
||||||
|
|
||||||
|
text: stringvariant: enum?
|
||||||
|
|
||||||
|
Textarea
|
||||||
|
|
||||||
|
Multi-line text input. Use { $bindState } on value for binding. Use checks for validation.
|
||||||
|
|
||||||
|
label: stringname: stringplaceholder: string?rows: number?value: string?checks: array?
|
||||||
|
|
||||||
|
Toggle
|
||||||
|
|
||||||
|
Toggle button. Use { $bindState } on pressed for state binding.
|
||||||
|
|
||||||
|
label: stringpressed: boolean?variant: enum?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
ToggleGroup
|
||||||
|
|
||||||
|
Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.
|
||||||
|
|
||||||
|
items: arraytype: enum?value: string?
|
||||||
|
|
||||||
|
on.change
|
||||||
|
|
||||||
|
Tooltip
|
||||||
|
|
||||||
|
Hover tooltip. Shows content on hover over text.
|
||||||
|
|
||||||
|
content: stringtext: string
|
||||||
|
|
||||||
|
live renderstatic code
|
||||||
|
|
||||||
|
export
|
||||||
|
|
||||||
|
### Contact Us
|
||||||
|
|
||||||
|
Name
|
||||||
|
|
||||||
|
Email
|
||||||
|
|
||||||
|
`npm install @json-render/core @json-render/react`
|
||||||
|
|
||||||
|
[Get Started](https://json-render.dev/docs) [GitHub](https://github.com/vercel-labs/json-render)
|
||||||
|
|
||||||
|
01
|
||||||
|
|
||||||
|
### Define Your Catalog
|
||||||
|
|
||||||
|
Set the guardrails. Define which components, actions, and data bindings AI can use.
|
||||||
|
|
||||||
|
02
|
||||||
|
|
||||||
|
### AI Generates
|
||||||
|
|
||||||
|
Describe what you want. AI generates JSON constrained to your catalog. Every interface is unique.
|
||||||
|
|
||||||
|
03
|
||||||
|
|
||||||
|
### Render Instantly
|
||||||
|
|
||||||
|
Stream the response. Your components render progressively as JSON arrives.
|
||||||
|
|
||||||
|
## Define your catalog
|
||||||
|
|
||||||
|
Components, actions, and validation functions.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { defineSchema, defineCatalog } from '@json-render/core';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const schema = defineSchema({ /* ... */ });
|
||||||
|
|
||||||
|
export const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
Card: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
hasChildren: true,
|
||||||
|
},
|
||||||
|
Metric: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
statePath: z.string(),
|
||||||
|
format: z.enum(['currency', 'percent']),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
export: { params: z.object({ format: z.string() }) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Show all
|
||||||
|
|
||||||
|
## AI generates JSON
|
||||||
|
|
||||||
|
Constrained output that your components render natively.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"root": "dashboard",
|
||||||
|
"elements": {
|
||||||
|
"dashboard": {
|
||||||
|
"type": "Card",
|
||||||
|
"props": {
|
||||||
|
"title": "Revenue Dashboard"
|
||||||
|
},
|
||||||
|
"children": ["revenue"]
|
||||||
|
},
|
||||||
|
"revenue": {
|
||||||
|
"type": "Metric",
|
||||||
|
"props": {
|
||||||
|
"label": "Total Revenue",
|
||||||
|
"statePath": "/metrics/revenue",
|
||||||
|
"format": "currency"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Export as Code
|
||||||
|
|
||||||
|
Export generated UI as standalone React components. No runtime dependencies required.
|
||||||
|
|
||||||
|
### Generated UI Tree
|
||||||
|
|
||||||
|
AI generates a JSON structure from the user's prompt.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"root": "card",
|
||||||
|
"elements": {
|
||||||
|
"card": {
|
||||||
|
"type": "Card",
|
||||||
|
"props": { "title": "Revenue" },
|
||||||
|
"children": ["metric", "chart"]
|
||||||
|
},
|
||||||
|
"metric": {
|
||||||
|
"type": "Metric",
|
||||||
|
"props": {
|
||||||
|
"label": "Total Revenue",
|
||||||
|
"statePath": "analytics/revenue",
|
||||||
|
"format": "currency"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chart": {
|
||||||
|
"type": "Chart",
|
||||||
|
"props": {
|
||||||
|
"statePath": "analytics/salesByRegion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Show all
|
||||||
|
|
||||||
|
### Exported React Code
|
||||||
|
|
||||||
|
Export as a standalone Next.js project with all components.
|
||||||
|
|
||||||
|
```
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Card, Metric, Chart } from "@/components/ui";
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
analytics: {
|
||||||
|
revenue: 125000,
|
||||||
|
salesByRegion: [\
|
||||||
|
{ label: "US", value: 45000 },\
|
||||||
|
{ label: "EU", value: 35000 },\
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<Card data={data} title="Revenue">
|
||||||
|
<Metric
|
||||||
|
data={data}
|
||||||
|
label="Total Revenue"
|
||||||
|
statePath="analytics/revenue"
|
||||||
|
format="currency"
|
||||||
|
/>
|
||||||
|
<Chart data={data} statePath="analytics/salesByRegion" />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Show all
|
||||||
|
|
||||||
|
The export includes`package.json`, component files, styles, and everything needed to run independently.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Generative UI
|
||||||
|
|
||||||
|
Generate dynamic, personalized interfaces from prompts with AI
|
||||||
|
|
||||||
|
### Guardrails
|
||||||
|
|
||||||
|
AI can only use components you define in the catalog
|
||||||
|
|
||||||
|
### Streaming
|
||||||
|
|
||||||
|
Progressive rendering as JSON streams from the model
|
||||||
|
|
||||||
|
### React & React Native
|
||||||
|
|
||||||
|
Render on web and mobile from the same catalog and spec format
|
||||||
|
|
||||||
|
### Data Binding
|
||||||
|
|
||||||
|
Connect props to state with $state, $item, $index, and two-way binding
|
||||||
|
|
||||||
|
### Code Export
|
||||||
|
|
||||||
|
Export as standalone React code with no runtime dependencies
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
`npm install @json-render/core @json-render/react`
|
||||||
|
|
||||||
|
[Documentation](https://json-render.dev/docs)
|
||||||
|
|
||||||
|
Ask AI `⌘I`
|
||||||
263
.firecrawl/kobalte-dialog.md
Normal file
263
.firecrawl/kobalte-dialog.md
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
# Dialog
|
||||||
|
|
||||||
|
A window overlaid on either the primary window or another dialog window. Content behind a modal dialog is inert, meaning that users cannot interact with it.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyts
|
||||||
|
import { Dialog } from "@kobalte/core/dialog";
|
||||||
|
// or
|
||||||
|
import { Root, Trigger, ... } from "@kobalte/core/dialog";
|
||||||
|
// or (deprecated)
|
||||||
|
import { Dialog } from "@kobalte/core";
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyts
|
||||||
|
import { Dialog } from "@kobalte/core/dialog";
|
||||||
|
// or
|
||||||
|
import { Root, Trigger, ... } from "@kobalte/core/dialog";
|
||||||
|
// or (deprecated)
|
||||||
|
import { Dialog } from "@kobalte/core";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Follows the [WAI ARIA Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/) design pattern.
|
||||||
|
- Supports modal and non-modal modes.
|
||||||
|
- Provides screen reader announcements via rendered title and description.
|
||||||
|
- Focus is trapped and scrolling is blocked while it is open.
|
||||||
|
- Pressing `Esc` closes the dialog.
|
||||||
|
- Can be controlled or uncontrolled.
|
||||||
|
|
||||||
|
## Anatomy
|
||||||
|
|
||||||
|
The dialog consists of:
|
||||||
|
|
||||||
|
- **Dialog:** Contains all the parts of a dialog.
|
||||||
|
- **Dialog.Trigger:** The button that opens the dialog.
|
||||||
|
- **Dialog.Portal:** Portals its children into the `body` when the dialog is open.
|
||||||
|
- **Dialog.Overlay:** The layer that covers the inert portion of the view when the dialog is open.
|
||||||
|
- **Dialog.Content:** Contains the content to be rendered when the dialog is open.
|
||||||
|
- **Dialog.CloseButton:** The button that closes the dialog.
|
||||||
|
- **Dialog.Title:** An accessible title to be announced when the dialog is opened.
|
||||||
|
- **Dialog.Description:** An optional accessible description to be announced when the dialog is opened.
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
<Dialog>
|
||||||
|
<Dialog.Trigger />
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.CloseButton />
|
||||||
|
<Dialog.Title />
|
||||||
|
<Dialog.Description />
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
<Dialog>
|
||||||
|
<Dialog.Trigger />
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.CloseButton />
|
||||||
|
<Dialog.Title />
|
||||||
|
<Dialog.Description />
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Open
|
||||||
|
|
||||||
|
index.tsxstyle.css
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Dialog } from "@kobalte/core/dialog";
|
||||||
|
import { CrossIcon } from "some-icon-library";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<Dialog.Trigger class="dialog__trigger">Open</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay class="dialog__overlay" />
|
||||||
|
<div class="dialog__positioner">
|
||||||
|
<Dialog.Content class="dialog__content">
|
||||||
|
<div class="dialog__header">
|
||||||
|
<Dialog.Title class="dialog__title">About Kobalte</Dialog.Title>
|
||||||
|
<Dialog.CloseButton class="dialog__close-button">
|
||||||
|
<CrossIcon />
|
||||||
|
</Dialog.CloseButton>
|
||||||
|
</div>
|
||||||
|
<Dialog.Description class="dialog__description">
|
||||||
|
Kobalte is a UI toolkit for building accessible web apps and design systems with
|
||||||
|
SolidJS. It provides a set of low-level UI components and primitives which can be the
|
||||||
|
foundation for your design system implementation.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Content>
|
||||||
|
</div>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Dialog } from "@kobalte/core/dialog";
|
||||||
|
import { CrossIcon } from "some-icon-library";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<Dialog.Trigger class="dialog__trigger">Open</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay class="dialog__overlay" />
|
||||||
|
<div class="dialog__positioner">
|
||||||
|
<Dialog.Content class="dialog__content">
|
||||||
|
<div class="dialog__header">
|
||||||
|
<Dialog.Title class="dialog__title">About Kobalte</Dialog.Title>
|
||||||
|
<Dialog.CloseButton class="dialog__close-button">
|
||||||
|
<CrossIcon />
|
||||||
|
</Dialog.CloseButton>
|
||||||
|
</div>
|
||||||
|
<Dialog.Description class="dialog__description">
|
||||||
|
Kobalte is a UI toolkit for building accessible web apps and design systems with
|
||||||
|
SolidJS. It provides a set of low-level UI components and primitives which can be the
|
||||||
|
foundation for your design system implementation.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Content>
|
||||||
|
</div>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Default open
|
||||||
|
|
||||||
|
An initial, uncontrolled open value can be provided using the `defaultOpen` prop.
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
<Dialog defaultOpen>...</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
<Dialog defaultOpen>...</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controlled open
|
||||||
|
|
||||||
|
The `open` prop can be used to make the open state controlled. The `onOpenChange` event is fired when the user presses the trigger, close button or overlay, and receives the new value.
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
function ControlledExample() {
|
||||||
|
const [open, setOpen] = createSignal(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open()} onOpenChange={setOpen}>
|
||||||
|
...
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
function ControlledExample() {
|
||||||
|
const [open, setOpen] = createSignal(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open()} onOpenChange={setOpen}>
|
||||||
|
...
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Dialog
|
||||||
|
|
||||||
|
`Dialog` is equivalent to the `Root` import from `@kobalte/core/dialog` (and deprecated `Dialog.Root`).
|
||||||
|
|
||||||
|
| Prop | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| open | `boolean`<br> The controlled open state of the dialog. |
|
||||||
|
| defaultOpen | `boolean`<br> The default open state when initially rendered. Useful when you do not need to control the open state. |
|
||||||
|
| onOpenChange | `(open: boolean) => void`<br> Event handler called when the open state of the dialog changes. |
|
||||||
|
| id | `string`<br> A unique identifier for the component. The id is used to generate id attributes for nested components. If no id prop is provided, a generated id will be used. |
|
||||||
|
| modal | `boolean`<br> Whether the dialog should be the only visible content for screen readers, when set to `true`: <br> \- interaction with outside elements will be disabled. <br> \- scroll will be locked. <br> \- focus will be locked inside the dialog content. <br> \- elements outside the dialog content will not be visible for screen readers. |
|
||||||
|
| preventScroll | `boolean`<br> Whether the scroll should be locked even if the dialog is not modal. |
|
||||||
|
| forceMount | `boolean`<br> Used to force mounting the dialog (portal, overlay and content) when more control is needed. Useful when controlling animation with SolidJS animation libraries. |
|
||||||
|
| translations | [`DialogIntlTranslations`](https://github.com/kobaltedev/kobalte/blob/main/packages/core/src/dialog/dialog.intl.ts)<br> Localization strings. |
|
||||||
|
|
||||||
|
### Dialog.Trigger
|
||||||
|
|
||||||
|
`Dialog.Trigger` consists of [Button](https://kobalte.dev/docs/core/components/button).
|
||||||
|
|
||||||
|
| Data attribute | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| data-expanded | Present when the dialog is open. |
|
||||||
|
| data-closed | Present when the dialog is close. |
|
||||||
|
|
||||||
|
`Dialog.Content` and `Dialog.Overlay` shares the same data-attributes.
|
||||||
|
|
||||||
|
### Dialog.Content
|
||||||
|
|
||||||
|
| Prop | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| onOpenAutoFocus | `(event: Event) => void`<br> Event handler called when focus moves into the component after opening. It can be prevented by calling `event.preventDefault`. |
|
||||||
|
| onCloseAutoFocus | `(event: Event) => void`<br> Event handler called when focus moves to the trigger after closing. It can be prevented by calling `event.preventDefault`. |
|
||||||
|
| onEscapeKeyDown | `(event: KeyboardEvent) => void`<br> Event handler called when the escape key is down. It can be prevented by calling `event.preventDefault`. |
|
||||||
|
| onPointerDownOutside | `(event: PointerDownOutsideEvent) => void`<br> Event handler called when a pointer event occurs outside the bounds of the component. It can be prevented by calling `event.preventDefault`. |
|
||||||
|
| onFocusOutside | `(event: FocusOutsideEvent) => void`<br> Event handler called when the focus moves outside the bounds of the component. It can be prevented by calling `event.preventDefault`. |
|
||||||
|
| onInteractOutside | `(event: InteractOutsideEvent) => void`<br> Event handler called when an interaction (pointer or focus event) happens outside the bounds of the component. It can be prevented by calling `event.preventDefault`. |
|
||||||
|
|
||||||
|
## Rendered elements
|
||||||
|
|
||||||
|
| Component | Default rendered element |
|
||||||
|
| --- | --- |
|
||||||
|
| `Dialog` | none |
|
||||||
|
| `Dialog.Trigger` | `button` |
|
||||||
|
| `Dialog.Portal` | `Portal` |
|
||||||
|
| `Dialog.Overlay` | `div` |
|
||||||
|
| `Dialog.Content` | `div` |
|
||||||
|
| `Dialog.CloseButton` | `button` |
|
||||||
|
| `Dialog.Title` | `h2` |
|
||||||
|
| `Dialog.Description` | `p` |
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
### Keyboard Interactions
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `Space` | When focus is on the trigger, opens/closes the dialog. |
|
||||||
|
| `Enter` | When focus is on the trigger, opens/closes the dialog. |
|
||||||
|
| `Tab` | Moves focus to the next focusable element. |
|
||||||
|
| `Shift` \+ `Tab` | Moves focus to the previous focusable element. |
|
||||||
|
| `Esc` | Closes the dialog and moves focus to the trigger. |
|
||||||
|
|
||||||
|
Previous[←Context Menu](https://kobalte.dev/docs/core/components/context-menu)Next[Dropdown Menu→](https://kobalte.dev/docs/core/components/dropdown-menu)
|
||||||
29
.firecrawl/kobalte-intro.md
Normal file
29
.firecrawl/kobalte-intro.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
Kobalte is a UI toolkit for building accessible web apps and design systems with SolidJS. It provides a set of low-level UI components and primitives which can be the foundation for your design system implementation.
|
||||||
|
|
||||||
|
## Key features
|
||||||
|
|
||||||
|
### Accessible
|
||||||
|
|
||||||
|
Components follow the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) whenever possible. Kobalte handle accessibility implementation details like ARIA attributes, focus management, and keyboard navigation.
|
||||||
|
|
||||||
|
### Composable
|
||||||
|
|
||||||
|
Kobalte provides granular access to each component parts, so you can wrap them and add your own event listeners, props, etc.
|
||||||
|
|
||||||
|
### Unstyled
|
||||||
|
|
||||||
|
Components are shipped with zero styles, allowing you to completely customize the look and feel. Bring your preferred styling solution (vanilla CSS, Tailwind, CSS-in-JS libraries, etc...).
|
||||||
|
|
||||||
|
## Acknowledgment
|
||||||
|
|
||||||
|
Kobalte would not have been possible without the prior art done by other meaningful projects from the frontend community including:
|
||||||
|
|
||||||
|
- Ariakit - [https://ariakit.org/](https://ariakit.org/)
|
||||||
|
- Radix UI - [https://www.radix-ui.com/](https://www.radix-ui.com/)
|
||||||
|
- React Aria - [https://react-spectrum.adobe.com/react-aria/](https://react-spectrum.adobe.com/react-aria/)
|
||||||
|
- Zag - [https://zagjs.com/](https://zagjs.com/)
|
||||||
|
- corvu - [https://corvu.dev/](https://corvu.dev/)
|
||||||
|
|
||||||
|
Next[Getting started→](https://kobalte.dev/docs/core/overview/getting-started)
|
||||||
355
.firecrawl/kobalte-polymorphism.md
Normal file
355
.firecrawl/kobalte-polymorphism.md
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
# Polymorphism
|
||||||
|
|
||||||
|
All component parts that render a DOM element have an `as` prop.
|
||||||
|
|
||||||
|
## The `as` prop
|
||||||
|
|
||||||
|
For simple use cases the `as` prop can be used, either with native HTML elements or custom Solid components:
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Tabs } from "@kobalte/core/tabs";
|
||||||
|
import { MyCustomButton } from "./components";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<Tabs.List>
|
||||||
|
{/* Render an anchor tag instead of the default button */}
|
||||||
|
<Tabs.Trigger value="one" as="a">
|
||||||
|
A Trigger
|
||||||
|
</Tabs.Trigger>
|
||||||
|
|
||||||
|
{/* Render MyCustomButton instead of the default button */}
|
||||||
|
<Tabs.Trigger value="one" as={MyCustomButton}>
|
||||||
|
Custom Button Trigger
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Content value="one">Content one</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Tabs } from "@kobalte/core/tabs";
|
||||||
|
import { MyCustomButton } from "./components";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<Tabs.List>
|
||||||
|
{/* Render an anchor tag instead of the default button */}
|
||||||
|
<Tabs.Trigger value="one" as="a">
|
||||||
|
A Trigger
|
||||||
|
</Tabs.Trigger>
|
||||||
|
|
||||||
|
{/* Render MyCustomButton instead of the default button */}
|
||||||
|
<Tabs.Trigger value="one" as={MyCustomButton}>
|
||||||
|
Custom Button Trigger
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Content value="one">Content one</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `as` prop callback
|
||||||
|
|
||||||
|
For more advanced use cases the `as` prop can accept a callback.
|
||||||
|
The main reason to use a callback over the normal `as` prop is being able to set props without interfering with Kobalte.
|
||||||
|
|
||||||
|
When using this pattern the following rules apply to the callback:
|
||||||
|
|
||||||
|
- You must spread the props forwarded to your callback onto your node/component.
|
||||||
|
- Custom props are passed as is from the parent.
|
||||||
|
- Kobalte options are not passed to the callback, only the resulting html attributes.
|
||||||
|
- You should set your event handlers on the parent and not inside your callback.
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Tabs } from "@kobalte/core/tabs";
|
||||||
|
import { MyCustomButton } from "./components";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<Tabs.List>
|
||||||
|
{/* The `value` prop is used by Kobalte and not passed to MyCustomButton */}
|
||||||
|
<Tabs.Trigger value="one" as={MyCustomButton}>
|
||||||
|
A Trigger
|
||||||
|
</Tabs.Trigger>
|
||||||
|
|
||||||
|
{/* The `value` prop is used by Kobalte and not passed to MyCustomButton */}
|
||||||
|
<Tabs.Trigger
|
||||||
|
value="one"
|
||||||
|
as={props => (
|
||||||
|
// The `value` prop is directly passed to MyCustomButton
|
||||||
|
<MyCustomButton value="custom" {...props} />
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Custom Button Trigger
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Content value="one">Content one</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Tabs } from "@kobalte/core/tabs";
|
||||||
|
import { MyCustomButton } from "./components";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<Tabs.List>
|
||||||
|
{/* The `value` prop is used by Kobalte and not passed to MyCustomButton */}
|
||||||
|
<Tabs.Trigger value="one" as={MyCustomButton}>
|
||||||
|
A Trigger
|
||||||
|
</Tabs.Trigger>
|
||||||
|
|
||||||
|
{/* The `value` prop is used by Kobalte and not passed to MyCustomButton */}
|
||||||
|
<Tabs.Trigger
|
||||||
|
value="one"
|
||||||
|
as={props => (
|
||||||
|
// The `value` prop is directly passed to MyCustomButton
|
||||||
|
<MyCustomButton value="custom" {...props} />
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Custom Button Trigger
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Content value="one">Content one</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can optionally use a type helper to get the exact types passed to your callback:
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Tabs, TabsTriggerOptions, TabsTriggerRenderProps } from "@kobalte/core/tabs";
|
||||||
|
import { PolymorphicCallbackProps } from "@kobalte/core/polymorphic";
|
||||||
|
|
||||||
|
<Tabs.Trigger
|
||||||
|
value="one"
|
||||||
|
as={(
|
||||||
|
props: PolymorphicCallbackProps<
|
||||||
|
MyCustomButtonProps,
|
||||||
|
TabsTriggerOptions,
|
||||||
|
TabsTriggerRenderProps
|
||||||
|
>,
|
||||||
|
) => (
|
||||||
|
// The `value` prop is directly passed to MyCustomButton
|
||||||
|
<MyCustomButton value="custom" {...props} />
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Custom Button Trigger
|
||||||
|
</Tabs.Trigger>;
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Tabs, TabsTriggerOptions, TabsTriggerRenderProps } from "@kobalte/core/tabs";
|
||||||
|
import { PolymorphicCallbackProps } from "@kobalte/core/polymorphic";
|
||||||
|
|
||||||
|
<Tabs.Trigger
|
||||||
|
value="one"
|
||||||
|
as={(
|
||||||
|
props: PolymorphicCallbackProps<
|
||||||
|
MyCustomButtonProps,
|
||||||
|
TabsTriggerOptions,
|
||||||
|
TabsTriggerRenderProps
|
||||||
|
>,
|
||||||
|
) => (
|
||||||
|
// The `value` prop is directly passed to MyCustomButton
|
||||||
|
<MyCustomButton value="custom" {...props} />
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Custom Button Trigger
|
||||||
|
</Tabs.Trigger>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event lifecycle
|
||||||
|
|
||||||
|
Setting custom event handlers on component will call your custom handler before Kobalte's.
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
This section is mainly for library author that want to build on top of Kobalte and expose the correct types
|
||||||
|
to your end users.
|
||||||
|
|
||||||
|
Every component that renders an HTML element has the following types:
|
||||||
|
|
||||||
|
- `ComponentOptions`
|
||||||
|
- `ComponentCommonProps<T>`
|
||||||
|
- `ComponentRenderProps`
|
||||||
|
- `ComponentProps<T>`
|
||||||
|
|
||||||
|
For example, `Tabs.Trigger` has the types `TabsTriggerOptions`, `TabsTriggerCommonProps<T>`,
|
||||||
|
`TabsTriggerRenderProps` and `TabsTriggerProps<T>`.
|
||||||
|
|
||||||
|
Components themselves accept props as `PolymorphicProps<T, ComponentProps>` where `T` is a generic
|
||||||
|
that extends `ValidComponent` and `ComponentProps` are the props of the Kobalte component.
|
||||||
|
This type allows components to accept Kobalte's props and all other props accepted by `T`.
|
||||||
|
|
||||||
|
### `ComponentOptions`
|
||||||
|
|
||||||
|
This type contains all custom props consumed by Kobalte, these props do not exist in HTML.
|
||||||
|
These are not passed to the HTML element nor to the `as` callback.
|
||||||
|
|
||||||
|
### `ComponentCommonProps<T>`
|
||||||
|
|
||||||
|
This type contains HTML attributes optionally accepted by the Kobalte component and will
|
||||||
|
be forwarded to the rendered DOM node. These are managed by Kobalte but can be customized by the end
|
||||||
|
user. It includes attributes such as `id`, `ref`, event handlers, etc. The generic is used by `ref` and event handlers,
|
||||||
|
by default it is `HTMLElement`.
|
||||||
|
|
||||||
|
### `ComponentRenderProps`
|
||||||
|
|
||||||
|
This type extends `ComponentCommonProps` and additionally contains attributes that are passed
|
||||||
|
to the DOM node and fully managed by Kobalte. You should never assign these yourself or set them on
|
||||||
|
the Kobalte component. Modifying these props will break your component's behavior and accessibility.
|
||||||
|
|
||||||
|
### `ComponentProps<T>`
|
||||||
|
|
||||||
|
This is the final type exported by components, it is equal to `ComponentOptions & Partial<ComponentCommonProps>`.
|
||||||
|
It combines all props expected by Kobalte's component. The generic is used by the CommonProps, by default it is `HTMLElement`.
|
||||||
|
|
||||||
|
### `PolymorphicProps<T, ComponentProps>`
|
||||||
|
|
||||||
|
If you're writing a custom component and want to expose Kobalte's `as` prop to the end user
|
||||||
|
and keep proper typing, be sure to use `PolymorphicProps<T, ComponentProps>` for your props type.
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Tabs, TabsTriggerProps } from "@kobalte/core/tabs";
|
||||||
|
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||||
|
|
||||||
|
// Optionally extend `TabsTriggerProps` if you wish to
|
||||||
|
// expose Kobalte props to your end user.
|
||||||
|
interface CustomProps<T extends ValidComponent = "button"> extends TabsTriggerProps<T> {
|
||||||
|
variant: "default" | "outline";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your generic `T` should extend ValidComponent and have a default value of the default DOM node.
|
||||||
|
function CustomTabsTrigger<T extends ValidComponent = "button">(
|
||||||
|
props: PolymorphicProps<T, CustomProps<T>>,
|
||||||
|
) {
|
||||||
|
// Typescript degrades typechecking when using generics, as long as we
|
||||||
|
// spread `others` to our element, we can effectively ignore them.
|
||||||
|
const [local, others] = splitProps(props as CustomProps, ["variant"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.Trigger
|
||||||
|
// Optional, will default to Kobalte otherwise.
|
||||||
|
// This should match with your generic `T` default.
|
||||||
|
as="button"
|
||||||
|
class={local.variant === "default" ? "default-trigger" : "outline-trigger"}
|
||||||
|
// Make sure to spread these props!
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Tabs, TabsTriggerProps } from "@kobalte/core/tabs";
|
||||||
|
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||||
|
|
||||||
|
// Optionally extend `TabsTriggerProps` if you wish to
|
||||||
|
// expose Kobalte props to your end user.
|
||||||
|
interface CustomProps<T extends ValidComponent = "button"> extends TabsTriggerProps<T> {
|
||||||
|
variant: "default" | "outline";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your generic `T` should extend ValidComponent and have a default value of the default DOM node.
|
||||||
|
function CustomTabsTrigger<T extends ValidComponent = "button">(
|
||||||
|
props: PolymorphicProps<T, CustomProps<T>>,
|
||||||
|
) {
|
||||||
|
// Typescript degrades typechecking when using generics, as long as we
|
||||||
|
// spread `others` to our element, we can effectively ignore them.
|
||||||
|
const [local, others] = splitProps(props as CustomProps, ["variant"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.Trigger
|
||||||
|
// Optional, will default to Kobalte otherwise.
|
||||||
|
// This should match with your generic `T` default.
|
||||||
|
as="button"
|
||||||
|
class={local.variant === "default" ? "default-trigger" : "outline-trigger"}
|
||||||
|
// Make sure to spread these props!
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you do not wish to allow changing the element type, you can simplify your types by making
|
||||||
|
props: `OverrideComponentProps<"button", CustomProps>`, replace `"button"` with the correct
|
||||||
|
tagname for other components, imported from `"@kobalte/utils"`.
|
||||||
|
|
||||||
|
If you also want to export exact types, you can re-export and extends component types:
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
export interface CustomTabsTriggerOptions extends TabsTriggerOptions {
|
||||||
|
variant: "default" | "outline";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomTabsTriggerCommonProps<T extends HTMLElement = HTMLElement> extends TabsTriggerCommonProps<T> {
|
||||||
|
// If you allow users to set classes and extend them.
|
||||||
|
//class: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomTabsTriggerRenderProps
|
||||||
|
extends CustomTabsTriggerCommonProps,
|
||||||
|
TabsTriggerRenderProps {
|
||||||
|
// If you do not allow users to set classes and manage all of them.
|
||||||
|
class: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomTabsTriggerProps<T extends ValidComponent = "button"> = CustomTabsTriggerOptions &
|
||||||
|
Partial<CustomTabsTriggerCommonProps<ElementOf<T>>>;
|
||||||
|
|
||||||
|
export function CustomTabsTrigger<T extends ValidComponent = "button">(
|
||||||
|
props: PolymorphicProps<T, CustomTabsTriggerProps<T>,
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
export interface CustomTabsTriggerOptions extends TabsTriggerOptions {
|
||||||
|
variant: "default" | "outline";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomTabsTriggerCommonProps<T extends HTMLElement = HTMLElement> extends TabsTriggerCommonProps<T> {
|
||||||
|
// If you allow users to set classes and extend them.
|
||||||
|
//class: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomTabsTriggerRenderProps
|
||||||
|
extends CustomTabsTriggerCommonProps,
|
||||||
|
TabsTriggerRenderProps {
|
||||||
|
// If you do not allow users to set classes and manage all of them.
|
||||||
|
class: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomTabsTriggerProps<T extends ValidComponent = "button"> = CustomTabsTriggerOptions &
|
||||||
|
Partial<CustomTabsTriggerCommonProps<ElementOf<T>>>;
|
||||||
|
|
||||||
|
export function CustomTabsTrigger<T extends ValidComponent = "button">(
|
||||||
|
props: PolymorphicProps<T, CustomTabsTriggerProps<T>,
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ElementOf<T>` is a helper from `"@kobalte/core/polymorphic"` that converts a tag name into its element
|
||||||
|
(e.g. `ElementOf<"button"> = HTMLButtonElement`).
|
||||||
|
|
||||||
|
Previous[←Animation](https://kobalte.dev/docs/core/overview/animation)Next[Server side rendering→](https://kobalte.dev/docs/core/overview/ssr)
|
||||||
1464
.firecrawl/kobalte-select.md
Normal file
1464
.firecrawl/kobalte-select.md
Normal file
File diff suppressed because it is too large
Load Diff
10
.firecrawl/kobalte-ssr.md
Normal file
10
.firecrawl/kobalte-ssr.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Server side rendering
|
||||||
|
|
||||||
|
## Usage with SolidStart
|
||||||
|
|
||||||
|
Kobalte works out of the box with [SolidStart](https://start.solidjs.com/).
|
||||||
|
|
||||||
|
Kobalte has been tested with `solid-js@1.8.15` and `@solidjs/start@0.6.1`, compatibility with
|
||||||
|
other versions is not guaranteed.
|
||||||
|
|
||||||
|
Previous[←Polymorphism](https://kobalte.dev/docs/core/overview/polymorphism)Next[Accordion→](https://kobalte.dev/docs/core/components/accordion)
|
||||||
315
.firecrawl/kobalte-styling.md
Normal file
315
.firecrawl/kobalte-styling.md
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
# Styling
|
||||||
|
|
||||||
|
Kobalte components are unstyled, allowing you to completely customize the look and feel. Bring your preferred styling solution (vanilla CSS, Tailwind, CSS-in-JS libraries, etc...).
|
||||||
|
|
||||||
|
## Styling a component part
|
||||||
|
|
||||||
|
All components and their parts accept a `class` prop. This class will be passed through to the DOM element. You can style a component part by targeting the `class` that you provide.
|
||||||
|
|
||||||
|
index.tsxstyle.css
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Popover as KPopover } from "@kobalte/core";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
export const Popover = () => {
|
||||||
|
return (
|
||||||
|
<KPopover>
|
||||||
|
<KPopover.Trigger class="popover__trigger">
|
||||||
|
Open
|
||||||
|
</KPopover.Trigger>
|
||||||
|
<KPopover.Content class="popover__content">
|
||||||
|
...
|
||||||
|
</KPopover.Content>
|
||||||
|
</KPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Popover as KPopover } from "@kobalte/core";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
export const Popover = () => {
|
||||||
|
return (
|
||||||
|
<KPopover>
|
||||||
|
<KPopover.Trigger class="popover__trigger">
|
||||||
|
Open
|
||||||
|
</KPopover.Trigger>
|
||||||
|
<KPopover.Content class="popover__content">
|
||||||
|
...
|
||||||
|
</KPopover.Content>
|
||||||
|
</KPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling a state
|
||||||
|
|
||||||
|
When a component or its parts can have multiple states, we automatically attach `data-*` attributes that represents the specific state. For example, a popover's trigger can have:
|
||||||
|
|
||||||
|
- `data-expanded` — When the popover is expanded.
|
||||||
|
- `data-disabled` — When the popover is disabled.
|
||||||
|
|
||||||
|
You can style a component state by targeting the `data-*` attributes added by Kobalte.
|
||||||
|
|
||||||
|
style.css
|
||||||
|
|
||||||
|
```
|
||||||
|
Copycss
|
||||||
|
.popover__trigger[data-disabled] {
|
||||||
|
/* The popover trigger style when disabled. */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copycss
|
||||||
|
.popover__trigger[data-disabled] {
|
||||||
|
/* The popover trigger style when disabled. */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using the TailwindCSS plugin
|
||||||
|
|
||||||
|
If you are using [TailwindCSS](https://tailwindcss.com/), you can use the `@kobalte/tailwindcss` plugin to target Kobalte's `data-*` attributes with modifiers like `ui-expanded:*`.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
npmyarnpnpm
|
||||||
|
|
||||||
|
```
|
||||||
|
Copybash
|
||||||
|
npm install @kobalte/tailwindcss
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copybash
|
||||||
|
npm install @kobalte/tailwindcss
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Add the plugin to your `tailwind.config.js` :
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyjs
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [\
|
||||||
|
// default prefix is "ui"\
|
||||||
|
require("@kobalte/tailwindcss"),\
|
||||||
|
\
|
||||||
|
// or with a custom prefix:\
|
||||||
|
require("@kobalte/tailwindcss")({ prefix: "kb" }),\
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyjs
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [\
|
||||||
|
// default prefix is "ui"\
|
||||||
|
require("@kobalte/tailwindcss"),\
|
||||||
|
\
|
||||||
|
// or with a custom prefix:\
|
||||||
|
require("@kobalte/tailwindcss")({ prefix: "kb" }),\
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Style your component:
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Popover as KPopover } from "@kobalte/core/popover";
|
||||||
|
|
||||||
|
export const Popover = () => (
|
||||||
|
<KPopover>
|
||||||
|
<KPopover.Trigger class="inline-flex px-4 py-2 rounded ui-disabled:bg-slate-100">
|
||||||
|
Open
|
||||||
|
</KPopover.Trigger>
|
||||||
|
<KPopover.Content class="flex p-4 rounded bg-white">...</KPopover.Content>
|
||||||
|
</KPopover>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Popover as KPopover } from "@kobalte/core/popover";
|
||||||
|
|
||||||
|
export const Popover = () => (
|
||||||
|
<KPopover>
|
||||||
|
<KPopover.Trigger class="inline-flex px-4 py-2 rounded ui-disabled:bg-slate-100">
|
||||||
|
Open
|
||||||
|
</KPopover.Trigger>
|
||||||
|
<KPopover.Content class="flex p-4 rounded bg-white">...</KPopover.Content>
|
||||||
|
</KPopover>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use the following modifiers:
|
||||||
|
|
||||||
|
| Modifier | CSS Selector |
|
||||||
|
| --- | --- |
|
||||||
|
| `ui-valid` | `&[data-valid]` |
|
||||||
|
| `ui-invalid` | `&[data-invalid]` |
|
||||||
|
| `ui-required` | `&[data-required]` |
|
||||||
|
| `ui-disabled` | `&[data-disabled]` |
|
||||||
|
| `ui-readonly` | `&[data-readonly]` |
|
||||||
|
| `ui-checked` | `&[data-checked]` |
|
||||||
|
| `ui-indeterminate` | `&[data-indeterminate]` |
|
||||||
|
| `ui-selected` | `&[data-selected]` |
|
||||||
|
| `ui-pressed` | `&[data-pressed]` |
|
||||||
|
| `ui-expanded` | `&[data-expanded]` |
|
||||||
|
| `ui-highlighted` | `&[data-highlighted]` |
|
||||||
|
| `ui-current` | `&[data-current]` |
|
||||||
|
|
||||||
|
It's also possible to use _inverse modifiers_ in the form of `ui-not-*`, _group and peer modifiers_ in the form of `ui-group-*` and `ui-peer-*`.
|
||||||
|
|
||||||
|
## Using the Vanilla Extract plugin
|
||||||
|
|
||||||
|
If you are using [Vanilla Extract](https://vanilla-extract.style/), you can use the `@kobalte/vanilla-extract` plugin to target Kobalte's `data-*` attributes.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
npmyarnpnpm
|
||||||
|
|
||||||
|
```
|
||||||
|
Copybash
|
||||||
|
npm install @kobalte/vanilla-extract
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copybash
|
||||||
|
npm install @kobalte/vanilla-extract
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Use the `componentStateStyles` utility function to create vanilla-extract styles that target `data-*` attributes of Kobalte components.
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyts
|
||||||
|
// styles.css
|
||||||
|
import { componentStateStyles } from "@kobalte/vanilla-extract";
|
||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
const button = style([\
|
||||||
|
{\
|
||||||
|
background: "blue",\
|
||||||
|
padding: "2px 6px",\
|
||||||
|
},\
|
||||||
|
componentStateStyles({\
|
||||||
|
disabled: {\
|
||||||
|
opacity: 0.4,\
|
||||||
|
},\
|
||||||
|
invalid: {\
|
||||||
|
backgroundColor: "red",\
|
||||||
|
not: {\
|
||||||
|
backgroundColor: "yellow",\
|
||||||
|
},\
|
||||||
|
},\
|
||||||
|
}),\
|
||||||
|
componentStateStyles(\
|
||||||
|
{\
|
||||||
|
invalid: {\
|
||||||
|
backgroundColor: "red",\
|
||||||
|
},\
|
||||||
|
},\
|
||||||
|
{ parentSelector: "[data-theme=dark]" },\
|
||||||
|
),\
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyts
|
||||||
|
// styles.css
|
||||||
|
import { componentStateStyles } from "@kobalte/vanilla-extract";
|
||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
const button = style([\
|
||||||
|
{\
|
||||||
|
background: "blue",\
|
||||||
|
padding: "2px 6px",\
|
||||||
|
},\
|
||||||
|
componentStateStyles({\
|
||||||
|
disabled: {\
|
||||||
|
opacity: 0.4,\
|
||||||
|
},\
|
||||||
|
invalid: {\
|
||||||
|
backgroundColor: "red",\
|
||||||
|
not: {\
|
||||||
|
backgroundColor: "yellow",\
|
||||||
|
},\
|
||||||
|
},\
|
||||||
|
}),\
|
||||||
|
componentStateStyles(\
|
||||||
|
{\
|
||||||
|
invalid: {\
|
||||||
|
backgroundColor: "red",\
|
||||||
|
},\
|
||||||
|
},\
|
||||||
|
{ parentSelector: "[data-theme=dark]" },\
|
||||||
|
),\
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then apply your styles to the component:
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Button } from "@kobalte/core/button";
|
||||||
|
import { button } from "./styles.css";
|
||||||
|
|
||||||
|
export const MyButton = () => <Button class={button}>...</Button>;
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Button } from "@kobalte/core/button";
|
||||||
|
import { button } from "./styles.css";
|
||||||
|
|
||||||
|
export const MyButton = () => <Button class={button}>...</Button>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage with UnoCSS
|
||||||
|
|
||||||
|
The [UnoCSS preset](https://github.com/zirbest/unocss-preset-primitives#kobalte) made by the community can be used to achieve the same behavior of the TailwindCSS plugin.
|
||||||
|
|
||||||
|
## Extending a component
|
||||||
|
|
||||||
|
Extending a component is done the same way you extend any SolidJS component.
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Popover as KPopover } from "@kobalte/core/popover";
|
||||||
|
import { ComponentProps } from "solid-js";
|
||||||
|
|
||||||
|
export const PopoverTrigger = (props: ComponentProps<typeof KPopover.Trigger>) => {
|
||||||
|
return <KPopover.Trigger {...props} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Copytsx
|
||||||
|
import { Popover as KPopover } from "@kobalte/core/popover";
|
||||||
|
import { ComponentProps } from "solid-js";
|
||||||
|
|
||||||
|
export const PopoverTrigger = (props: ComponentProps<typeof KPopover.Trigger>) => {
|
||||||
|
return <KPopover.Trigger {...props} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Previous[←Getting started](https://kobalte.dev/docs/core/overview/getting-started)Next[Animation→](https://kobalte.dev/docs/core/overview/animation)
|
||||||
381
.firecrawl/logrocket-ai-shadcn.md
Normal file
381
.firecrawl/logrocket-ai-shadcn.md
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
[**Advisory boards aren’t only for executives. Join the LogRocket Content Advisory Board today →**](https://lp.logrocket.com/blg/content-advisory-board-signup)
|
||||||
|
|
||||||
|
[](https://logrocket.com/)
|
||||||
|
|
||||||
|
2025-10-03
|
||||||
|
|
||||||
|
1610
|
||||||
|
|
||||||
|
#ai
|
||||||
|
|
||||||
|
Chizaram Ken
|
||||||
|
|
||||||
|
207953
|
||||||
|
|
||||||
|
102
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## See how LogRocket's Galileo AI surfaces the most severe issues for you
|
||||||
|
|
||||||
|
### No signup required
|
||||||
|
|
||||||
|
Check it out
|
||||||
|
|
||||||
|
Galileo AI Overview - May 2025
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1:15
|
||||||
|
|
||||||
|
Click for sound
|
||||||
|
|
||||||
|
You ask Claude Code or Cursor about a shadcn/ui component, and it’ll confidently spit out props that don’t exist, dust off patterns from 2023, or just flat-out make things up.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Most of the time, this comes down to version changes. shadcn/ui keeps evolving, new props, updated requirements, and agents often lean on older docs or outdated patterns.
|
||||||
|
|
||||||
|
Other times, it’s simply the AI guessing. This is the not-so-smart side of AI: it won’t admit “I don’t know,” so it stitches together whatever scraps it half-remembers from training instead of the actual component code.
|
||||||
|
|
||||||
|
Case in point: your agent might suggest `<Button loading={true}>` even though shadcn/ui’s Button has no `loading` prop. It’s pulling from some other UI library in the background.
|
||||||
|
|
||||||
|
The truth is, it guesses because it has almost zero library context. And that’s exactly why we’re going to look at the shadcn/ui [MCP server](https://ui.shadcn.com/docs/mcp), to give your agent real, live access to the component library instead of making it wing it.
|
||||||
|
|
||||||
|
Before we dive in, let’s set the stage. The goal of this article is simple: show you how to use the shadcn/ui MCP Server in your workflow so your AI agent stops generating broken components. With the right setup, you’ll get reliable, up-to-date ShadCN code instead of outdated patterns or random guesses.
|
||||||
|
|
||||||
|
### 🚀 Sign up for The Replay newsletter
|
||||||
|
|
||||||
|
[**The Replay**](https://blog.logrocket.com/the-replay-archive/) is a weekly newsletter for dev and engineering leaders.
|
||||||
|
|
||||||
|
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
|
||||||
|
|
||||||
|
Fields marked with an \* are required
|
||||||
|
|
||||||
|
Email \*
|
||||||
|
|
||||||
|
If you are a human seeing this field, please leave it empty.
|
||||||
|
|
||||||
|
|
||||||
|
## **ShadCN MCP Server**
|
||||||
|
|
||||||
|
The ShadCN MCP (Model Context Protocol) server acts as a link between AI agents and component registries, and it fundamentally changes how AI agents interact with component libraries. Unlike [other ShadCN MCPs](https://github.com/Jpisnice/shadcn-ui-mcp-server) that provide only data for ShadCN components, this official MCP server provides more recent access to:
|
||||||
|
|
||||||
|
### **Live component access**
|
||||||
|
|
||||||
|
AI assistants gain direct connection to current component specifications, ensuring they always work with the latest versions and configurations.
|
||||||
|
|
||||||
|
### **Registry integration**
|
||||||
|
|
||||||
|
The server connects to multiple component sources, this includes the official shadcn/ui registry, giving your AI agents access to more detailed data of components needed for your project. So, you are no longer limited to just ShadCN.
|
||||||
|
|
||||||
|
### **Accurate installation**
|
||||||
|
|
||||||
|
AI Agents can now interpret conversational prompts like “add a login form” or “create a contact form using Shadcn components” and translate them into proper registry commands and needed installations.
|
||||||
|
|
||||||
|
### **Better component selection**
|
||||||
|
|
||||||
|
The server enables AI Agents to search the available components and make more informed decisions about which components best fit specific requirements.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## **Quick setup**
|
||||||
|
|
||||||
|
Setting up the Shadcn MCP server is a straightforward process for any major AI coding environment. We will be using Claude-code for this, go ahead and install it using the command below:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://claude.ai/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Now open up your CLI, prompt `Claude`. You should know Claude is available if you see this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Now go ahead and install the MCP server by running the command below in your project directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest mcp init --client claude
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Go ahead to restart Claude Code now. If it was properly installed, you should see this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
After that, you can use the `/mcp` command, and you will be able to see the MCP tools:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
There are about seven tools available. You can immediately start using prompts like “Show me all available components in the ShadCN registry” or “Add the button, dialog, and card components to my project.”
|
||||||
|
|
||||||
|
### **Configuration for other environments like Cursor**
|
||||||
|
|
||||||
|
Add the MCP server configuration to your project’s MCP configuration file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"shadcn": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["shadcn@latest", "mcp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Multi-registry support**
|
||||||
|
|
||||||
|
Configure additional registries in your `components.json` to access private or third-party component libraries:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"registries": {
|
||||||
|
"@acme": "https://registry.acme.com/{name}.json",
|
||||||
|
"@internal": {
|
||||||
|
"url": "https://internal.company.com/{name}.json",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer ${REGISTRY_TOKEN}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point, we can go ahead to build something that actually utilizes a whole lot of ShanCn components, like a kanban board.
|
||||||
|
|
||||||
|
## **Building Kanban board**
|
||||||
|
|
||||||
|
We want to build a Kanban board for my article writing workflow at LogRocket Blog. Here’s my complete process from initial research to publication:
|
||||||
|
|
||||||
|
### **Topic research**
|
||||||
|
|
||||||
|
- Search for topics I’m familiar with and passionate about
|
||||||
|
- Check the LogRocket Blog archives to see if the topic has been covered
|
||||||
|
- Assess coverage depth – is there room for a fresh perspective or deeper dive?
|
||||||
|
|
||||||
|
### **Content strategy**
|
||||||
|
|
||||||
|
- Define learning outcomes – What will readers gain from investing 8 minutes in this piece?
|
||||||
|
- Determine tone and approach – Should this be highly technical and direct, or include light humor to ease the reader’s tension?
|
||||||
|
- Research competitor content and identify unique angles
|
||||||
|
|
||||||
|
### **Draft an outline**
|
||||||
|
|
||||||
|
- Draft a detailed article structure
|
||||||
|
- Submit the outline to the manager for review and approval
|
||||||
|
- Gain approval and work on considerable feedback from the manager
|
||||||
|
|
||||||
|
### **Create content**
|
||||||
|
|
||||||
|
- Write the approved article following the outlined structure(Sometimes you make a lot of tweaks for better quality)
|
||||||
|
- Ensure technical accuracy and readers’ engagement
|
||||||
|
- Self-review for clarity and smooth flow
|
||||||
|
|
||||||
|
### **Editorial process**
|
||||||
|
|
||||||
|
- Submit to the editing team for review
|
||||||
|
- Address comments and suggestions from the editor (there’s always at least one!)
|
||||||
|
- Finalize revisions and prepare for publication
|
||||||
|
|
||||||
|
### **Publication & launch**
|
||||||
|
|
||||||
|
- Article goes live on LogRocket Blog
|
||||||
|
- Monitor initial reader engagement and feedback
|
||||||
|
|
||||||
|
Let’s go ahead and create the Kanban board for this workflow. Here is a detailed prompt that embodies our workflow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Build a Kanban board component for my LogRocket Blog article writing workflow. I need a drag-and-drop board with 6 columns representing my workflow stages:
|
||||||
|
|
||||||
|
- Topic Research - For initial topic exploration and validation
|
||||||
|
- Content Strategy - For planning learning outcomes and approach
|
||||||
|
- Draft Outline - For creating and getting approval on article structure
|
||||||
|
- Create Content - For writing the actual article
|
||||||
|
- Editorial Process - For editing, revisions, and feedback
|
||||||
|
- Publication & Launch - For live articles and monitoring
|
||||||
|
|
||||||
|
Each column should:
|
||||||
|
- Display the stage name clearly
|
||||||
|
- Show a count of cards in that column
|
||||||
|
- Allow drag-and-drop functionality between columns
|
||||||
|
- Support adding new article cards
|
||||||
|
|
||||||
|
Each article card should include:
|
||||||
|
- Article title/topic
|
||||||
|
- Brief description or notes
|
||||||
|
- Priority indicator (high/medium/low)
|
||||||
|
- Due date or target timeline
|
||||||
|
- Current status within that stage
|
||||||
|
|
||||||
|
Please use ShadCN components like Card, Badge, Button, and any drag-and-drop utilities available, if you do not find the exact names of these components use something components that are very similar. Make it clean, professional, and suitable for a content writer's daily workflow. Include sample article cards in different stages to demonstrate the workflow.
|
||||||
|
```
|
||||||
|
|
||||||
|
We will feed this to Claude-code and see what the result is like. Right away, it goes to work:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
We’re done, I guess:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This is what the first result looks like:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
I feel Claude omitted the point where ShadCN components need CSS variables defined. Will tell it to fix that.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
[\\
|
||||||
|
\\
|
||||||
|
**Over 200k developers use LogRocket to create better digital experiences** \\
|
||||||
|
\\
|
||||||
|
Learn more →](https://lp.logrocket.com/blg/learn-more)
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here is what the UI looks like now:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
I wanted to see how different this would have been without the MCP server, so I did another test with Gemini CLI. Here was how the first result came out:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here is the final result:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Same prompt, same number of iterations, but not the same results, and that exactly explains why you should utilize the ShadCN MCP server for your next project, that’s if you choose to use ShadCN as your UI library, though.
|
||||||
|
|
||||||
|
## **Troubleshooting**
|
||||||
|
|
||||||
|
Here are some common issues you might encounter:
|
||||||
|
|
||||||
|
### **MCP Server not responding**
|
||||||
|
|
||||||
|
When your MCP server isn’t picking up your prompts:
|
||||||
|
|
||||||
|
- **Check your configuration** – Make sure the MCP server is properly set up in your `.mcp.json` file and enabled in your client
|
||||||
|
- **Restart your client** – After any configuration changes, restart Claude Code, Cursor, Windsurf, or VS Code completely
|
||||||
|
- **Verify ShadCN installation** – Ensure you have ShadCN properly installed in your project directory
|
||||||
|
- **Test network access** – Confirm you can reach the configured registries from your development environment
|
||||||
|
|
||||||
|
### **Registry access problems**
|
||||||
|
|
||||||
|
If components aren’t loading from your registries:
|
||||||
|
|
||||||
|
- **Double-check components.json** – Verify your registry URLs are formatted correctly and accessible
|
||||||
|
- **Test authentication** – Make sure environment variables are properly set for private registries in your `.env.local`
|
||||||
|
- **Confirm registry status** – Check that the registry is online and responding
|
||||||
|
- **Validate namespace syntax** – Ensure you’re using the correct `@namespace/component` format
|
||||||
|
|
||||||
|
### **Installation failures**
|
||||||
|
|
||||||
|
When components refuse to install:
|
||||||
|
|
||||||
|
- **Verify project setup** – Confirm you have a valid `components.json` file in your project root
|
||||||
|
- **Check directory paths** – Make sure target directories exist and are writable
|
||||||
|
- **Review permissions** – Ensure you have write permissions for component directories
|
||||||
|
|
||||||
|
### **No tools available**
|
||||||
|
|
||||||
|
If you’re seeing “No tools or prompts” messages:
|
||||||
|
|
||||||
|
- **Clear NPX cache** – Run `npx clear-npx-cache` to refresh cached packages
|
||||||
|
- **Re-enable MCP Server** – Try disabling and re-enabling the MCP server in your client settings
|
||||||
|
- **Check logs** – In Cursor or Windsurf, go to View → Output and select “MCP: project-” from the dropdown to see detailed logs
|
||||||
|
|
||||||
|
Based on the shadcn/ui MCP Server Documentation and troubleshooting experience. There are other ShadCN MCP servers; you can try [this as well](https://github.com/Jpisnice/shadcn-ui-mcp-server).
|
||||||
|
|
||||||
|
## **Conclusion**
|
||||||
|
|
||||||
|
The gap between AI agents with and without access to live component docs is huge. With the shadcn/ui MCP server, Claude Code delivered accurate, working components aligned with the latest specs. Without it, Gemini CLI slipped into outdated patterns and even made up props that never existed.
|
||||||
|
|
||||||
|
This goes beyond convenience; it’s about reliability. The MCP server cuts out the cycle of AI-generated code that looks right but fails at runtime because it’s based on guesswork, not facts.
|
||||||
|
|
||||||
|
For developers using shadcn/ui, the setup takes just a few minutes and can save hours of debugging broken components. With MCP in place, your AI coding assistant stops guessing and starts acting like a partner that actually understands the library.
|
||||||
|
|
||||||
|
Learn more about MCPs here:
|
||||||
|
|
||||||
|
- [Understanding Anthropic’s Model Context Protocol (MCP)](https://blog.logrocket.com/understanding-anthropic-model-context-protocol-mcp/)
|
||||||
|
- [The top 15 MCP servers for your AI projects](https://blog.logrocket.com/top-15-mcp-servers-ai-projects/)
|
||||||
|
|
||||||
|
- [#ai](https://blog.logrocket.com/tag/ai/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Stop guessing about your digital experience with LogRocket
|
||||||
|
|
||||||
|
[Get started for free](https://lp.logrocket.com/blg/signup)
|
||||||
|
|
||||||
|
#### Recent posts:
|
||||||
|
|
||||||
|
[\\
|
||||||
|
**The Replay (3/18/26): Hiring in the AI era, coding isn’t dead, and more**](https://blog.logrocket.com/the-replay-3-18-26/)
|
||||||
|
|
||||||
|
Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the March 18th issue.
|
||||||
|
|
||||||
|
[](https://blog.logrocket.com/author/matthewmaccormack/)[Matt MacCormack](https://blog.logrocket.com/author/matthewmaccormack/)
|
||||||
|
|
||||||
|
Mar 18, 2026 ⋅ 29 sec read
|
||||||
|
|
||||||
|
[\\
|
||||||
|
**Thinking beats coding: How to hire the right engineers in the AI era**](https://blog.logrocket.com/how-to-hire-the-right-engineers-in-the-ai-era/)
|
||||||
|
|
||||||
|
A CTO outlines his case for how leaders should prioritize complex thinking over framework knowledge when hiring engineers for the AI era.
|
||||||
|
|
||||||
|
[](https://blog.logrocket.com/author/ken_pickering/)[Ken Pickering](https://blog.logrocket.com/author/ken_pickering/)
|
||||||
|
|
||||||
|
Mar 18, 2026 ⋅ 4 min read
|
||||||
|
|
||||||
|
[\\
|
||||||
|
**Exploring Vercel’s JSON Render: build dynamic UI from structured data**](https://blog.logrocket.com/vercel-json-render-dynamic-ui/)
|
||||||
|
|
||||||
|
Build dynamic, AI-generated UI safely with Vercel’s JSON Render using structured JSON, validated components, and React.
|
||||||
|
|
||||||
|
[](https://blog.logrocket.com/author/emmanueljohn/)[Emmanuel John](https://blog.logrocket.com/author/emmanueljohn/)
|
||||||
|
|
||||||
|
Mar 17, 2026 ⋅ 11 min read
|
||||||
|
|
||||||
|
[\\
|
||||||
|
**Stop wasting money on AI: 10 ways to cut token usage**](https://blog.logrocket.com/stop-wasting-ai-tokens-10-ways-to-reduce-usage/)
|
||||||
|
|
||||||
|
Learn practical techniques to reduce token usage in LLM applications and build more cost-efficient, scalable AI systems.
|
||||||
|
|
||||||
|
[](https://blog.logrocket.com/author/emmanueljohn/)[Emmanuel John](https://blog.logrocket.com/author/emmanueljohn/)
|
||||||
|
|
||||||
|
Mar 16, 2026 ⋅ 8 min read
|
||||||
|
|
||||||
|
[View all posts](https://blog.logrocket.com/)
|
||||||
|
|
||||||
|
### Leave a Reply [Cancel reply](https://blog.logrocket.com/ai-shadcn-components\#respond)
|
||||||
|
|
||||||
|
Your email address will not be published.Required fields are marked \*
|
||||||
|
|
||||||
|
Comment \*
|
||||||
|
|
||||||
|
Name \*
|
||||||
|
|
||||||
|
Email \*
|
||||||
|
|
||||||
|
Website
|
||||||
|
|
||||||
|
Save my name, email, and website in this browser for the next time I comment.
|
||||||
|
|
||||||
|
Would you be interested in joining LogRocket's developer community?
|
||||||
|
|
||||||
|
YeaNo Thanks
|
||||||
|
|
||||||
|
Join LogRocket’s Content Advisory Board. You’ll help inform the type of
|
||||||
|
content we create and get access to exclusive meetups, social accreditation,
|
||||||
|
and swag.
|
||||||
|
|
||||||
|
|
||||||
|
[Sign up now](https://lp.logrocket.com/blg/content-advisory-board-signup)
|
||||||
1361
.firecrawl/logrocket-headless-alternatives.md
Normal file
1361
.firecrawl/logrocket-headless-alternatives.md
Normal file
File diff suppressed because it is too large
Load Diff
934
.firecrawl/logrocket-headless-ui.md
Normal file
934
.firecrawl/logrocket-headless-ui.md
Normal file
@ -0,0 +1,934 @@
|
|||||||
|
[**Advisory boards aren’t only for executives. Join the LogRocket Content Advisory Board today →**](https://lp.logrocket.com/blg/content-advisory-board-signup)
|
||||||
|
|
||||||
|
[](https://logrocket.com/)
|
||||||
|
|
||||||
|
2026-03-02
|
||||||
|
|
||||||
|
3006
|
||||||
|
|
||||||
|
#react
|
||||||
|
|
||||||
|
Amazing Enyichi Agu
|
||||||
|
|
||||||
|
192451
|
||||||
|
|
||||||
|
116
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## See how LogRocket's Galileo AI surfaces the most severe issues for you
|
||||||
|
|
||||||
|
### No signup required
|
||||||
|
|
||||||
|
Check it out
|
||||||
|
|
||||||
|
Galileo AI Overview - May 2025
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1:15
|
||||||
|
|
||||||
|
Click for sound
|
||||||
|
|
||||||
|
_**Editor’s note:** This post was updated in March 2026 by [Elijah Asoula](https://blog.logrocket.com/author/asaoluelijah/) to include Base UI and add updated examples and use cases to make the comparison more actionable._
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Using React component libraries is a popular way to quickly build React applications. Components from these libraries offer several advantages. First, they follow accessibility guidelines such as [WAI-ARIA](https://www.w3.org/WAI/standards-guidelines/aria/), ensuring that applications are usable by everyone. Second, they come with built-in styling and design so developers can focus on other aspects of their applications. Third, many include pre-defined behaviors — for example, an autocomplete component that filters options based on user input — which saves time and effort compared to building from scratch.
|
||||||
|
|
||||||
|
React component libraries are also typically optimized for performance. Because they are maintained by large communities or organizations, they receive regular updates and follow efficient coding practices. Examples include [Material UI](https://blog.logrocket.com/guide-material-design-react/), [Chakra UI](https://blog.logrocket.com/chakra-ui-adoption-guide/), and [React Bootstrap](https://www.youtube.com/watch?v=NlZUtfNVAkI).
|
||||||
|
|
||||||
|
However, these libraries leave limited room for customization. You can usually tweak styles, but you cannot fundamentally change the underlying design system. A developer may want the accessibility and functionality benefits of a component library while still implementing a completely custom design system.
|
||||||
|
|
||||||
|
Headless (unstyled) component libraries were created to fill this gap. A headless component library provides fully functional components without imposing styling. With headless components, developers are responsible for styling them however they see fit.
|
||||||
|
|
||||||
|
Tailwind Labs’ [Headless UI](https://headlessui.com/) is one of the most popular headless libraries in the React ecosystem. While it works well for many projects, it is not always the best choice for every use case. This article explores several alternatives for unstyled components, including Radix Primitives, React Aria, Ark UI, and Base UI.
|
||||||
|
|
||||||
|
### 🚀 Sign up for The Replay newsletter
|
||||||
|
|
||||||
|
[**The Replay**](https://blog.logrocket.com/the-replay-archive/) is a weekly newsletter for dev and engineering leaders.
|
||||||
|
|
||||||
|
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
|
||||||
|
|
||||||
|
Fields marked with an \* are required
|
||||||
|
|
||||||
|
Email \*
|
||||||
|
|
||||||
|
If you are a human seeing this field, please leave it empty.
|
||||||
|
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
To follow along with this guide, you should have a basic understanding of HTML, CSS, JavaScript, and React.
|
||||||
|
|
||||||
|
## Why not just use Tailwind Labs’ Headless UI library?
|
||||||
|
|
||||||
|
Headless UI is an unstyled React component library developed by Tailwind Labs, the creators of Tailwind CSS. The library is designed to integrate particularly well with Tailwind CSS, as noted in its documentation. It is also one of the most widely adopted headless libraries, with around 28K GitHub stars and millions of weekly npm downloads.
|
||||||
|
|
||||||
|
However, Headless UI is limited in the number of unstyled components it provides. At the time of writing, it offers 16 primary components. The other libraries covered in this article provide significantly more components for broader use cases. Additionally, some of these alternatives include utility components and helper functions that Headless UI does not offer.
|
||||||
|
|
||||||
|
Let’s explore these alternatives.
|
||||||
|
|
||||||
|
## Radix Primitives
|
||||||
|
|
||||||
|
[Radix Primitives](https://www.radix-ui.com/primitives) is a library of unstyled React components built by the team behind [Radix UI](https://radix-ui.com/), a UI library with fully styled and customizable components. According to its website, the Node.js, Vercel, and Supabase teams use Radix Primitives. The project has approximately 18K stars on [GitHub](https://github.com/radix-ui/primitives).
|
||||||
|
|
||||||
|
You can [style Radix Primitives components](https://blog.logrocket.com/radix-ui-adoption-guide/#:~:text=you%20should%20know.-,Radix%20Primitives,-Radix%20Primitives%20is) using any styling solution, including CSS, Tailwind CSS, or CSS-in-JS. The components also support server-side rendering. Radix provides comprehensive documentation for each primitive, explaining usage patterns and composition strategies.
|
||||||
|
|
||||||
|
### Installing and using Radix Primitives
|
||||||
|
|
||||||
|
The following steps demonstrate how to install and use Radix Primitives. In this example, we’ll import a dialog component and style it using vanilla CSS.
|
||||||
|
|
||||||
|
First, [create a React project](https://react.dev/learn/creating-a-react-app) using your preferred framework, or open an existing project.
|
||||||
|
|
||||||
|
Next, install the Radix primitive you need. Radix publishes each component as a separate package. For this example, install the `Dialog` component:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @radix-ui/react-dialog
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, create a file to import and customize the unstyled component:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// RadixDialog.jsx
|
||||||
|
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import './radix.style.css';
|
||||||
|
|
||||||
|
function RadixDialog() {
|
||||||
|
return (
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger className='btn primary-btn'>
|
||||||
|
Radix Dialog
|
||||||
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className='dialog-overlay' />
|
||||||
|
|
||||||
|
<Dialog.Content className='dialog-content'>
|
||||||
|
<Dialog.Title className='dialog-title'>
|
||||||
|
Confirm Deletion
|
||||||
|
</Dialog.Title>
|
||||||
|
|
||||||
|
<Dialog.Description className='dialog-body'>
|
||||||
|
Are you sure you want to permanently delete this file?
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<div className='bottom-btns'>
|
||||||
|
<Dialog.Close className='btn'>Cancel</Dialog.Close>
|
||||||
|
<Dialog.Close className='btn red-btn'>Delete Forever</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RadixDialog;
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, add styling:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* radix.style.css */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1.2rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: #1e64e7;
|
||||||
|
color: white;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-btn {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-overlay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
animation: overlayAnimation 200ms cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
background-color: white;
|
||||||
|
position: fixed;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
translate: -50% -50%;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 450px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
|
||||||
|
rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 3px solid #dfdddd;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-btns .btn:last-child {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes overlayAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, export and render the component in the DOM.
|
||||||
|
|
||||||
|
Here is the UI demo of the dialog component we styled above:
|
||||||
|
|
||||||
|
Dialog box built using Radix Primitives styled with custom CSS
|
||||||
|
|
||||||
|
### Radix Primitives pros and cons
|
||||||
|
|
||||||
|
Like every headless library covered in this guide, Radix Primitives has both advantages and tradeoffs.
|
||||||
|
|
||||||
|
**Pros**
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
[\\
|
||||||
|
\\
|
||||||
|
**Over 200k developers use LogRocket to create better digital experiences** \\
|
||||||
|
\\
|
||||||
|
Learn more →](https://lp.logrocket.com/blg/learn-more)
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
- It offers 28 main components, significantly more than Headless UI.
|
||||||
|
- You can install components individually, allowing incremental adoption.
|
||||||
|
- It provides an `asChild` prop that lets developers change the default DOM element of a Radix component — a pattern known as [composition](https://www.radix-ui.com/primitives/docs/guides/composition).
|
||||||
|
|
||||||
|
**Cons**
|
||||||
|
|
||||||
|
- Installing multiple components individually can feel repetitive.
|
||||||
|
- The anatomy-based structure of components can take time to understand.
|
||||||
|
|
||||||
|
## React Aria
|
||||||
|
|
||||||
|
[React Aria](https://react-spectrum.adobe.com/react-aria/index.html) is a library of unstyled components released by Adobe as part of its React UI collection, [React Spectrum](https://github.com/adobe/react-spectrum). While Adobe does not maintain a separate repository exclusively for React Aria, the React Spectrum repository has over 14K GitHub stars at the time of writing. Its npm package, `react-aria-components`, receives thousands of weekly downloads.
|
||||||
|
|
||||||
|
React Aria allows developers to style components using any preferred styling method. It also supports incremental adoption through [React Aria hooks](https://react-spectrum.adobe.com/react-aria/hooks.html), enabling fine-grained control over component behavior.
|
||||||
|
|
||||||
|
### Installing and using React Aria
|
||||||
|
|
||||||
|
In this example, we’ll build another dialog box using React Aria, styled similarly to the Radix example.
|
||||||
|
|
||||||
|
First, create a new React application or open an existing project. Then install the component package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react-aria-components
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, import the required components to construct a dialog:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// AriaDialog.jsx
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
Heading,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay
|
||||||
|
} from 'react-aria-components';
|
||||||
|
|
||||||
|
import './aria.style.css';
|
||||||
|
|
||||||
|
function AriaDialog() {
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button className='btn primary-btn'>
|
||||||
|
React Aria Dialog
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<ModalOverlay isDismissable>
|
||||||
|
<Modal>
|
||||||
|
<Dialog>
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<Heading slot='title'>
|
||||||
|
Confirm Deletion
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<p className='dialog-body'>
|
||||||
|
Are you sure you want to permanently delete this file?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className='bottom-btns'>
|
||||||
|
<Button className='btn' onPress={close}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button className='btn red-btn' onPress={close}>
|
||||||
|
Delete Forever
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</ModalOverlay>
|
||||||
|
</DialogTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AriaDialog;
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, add styling. React Aria provides built-in class names such as `.react-aria-Button`, which you can use directly in CSS. You can also override them with custom classes like `.btn` in this example:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* aria.style.css */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1.2rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: #1e64e7;
|
||||||
|
color: white;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-btn {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-aria-ModalOverlay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
animation: overlayAnimation 200ms cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-aria-Dialog {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 450px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
|
||||||
|
rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-aria-Dialog .react-aria-Heading {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 3px solid #dfdddd;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-btns .btn:last-child {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes overlayAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, export and render the component in the DOM.
|
||||||
|
|
||||||
|
Here is the output of the dialog box in this example:
|
||||||
|
|
||||||
|
Dialog component built using React Aria styled with custom CSS
|
||||||
|
|
||||||
|
### React Aria pros and cons
|
||||||
|
|
||||||
|
**Pros**
|
||||||
|
|
||||||
|
- It offers hooks for individual components, which support incremental adoption.
|
||||||
|
- It provides 43 main components.
|
||||||
|
- All components include built-in class names, simplifying styling.
|
||||||
|
|
||||||
|
**Cons**
|
||||||
|
|
||||||
|
- Some components require more setup. For example, the dialog required destructuring the `close` function and explicitly wiring it to buttons.
|
||||||
|
- Components often need to be combined to function fully. In this example, we used `Button`, `Dialog`, `DialogTrigger`, `Heading`, `Modal`, and `ModalOverlay` together to build a dialog. This structure can feel complex at first.
|
||||||
|
|
||||||
|
## Ark UI
|
||||||
|
|
||||||
|
[Ark UI](https://ark-ui.com/) is a library of unstyled components that work across React, Vue, and Solid. It is developed by Chakra Systems, the team behind Chakra UI. The project has gained steady adoption, with around 4.9K stars on [GitHub](https://github.com/chakra-ui/ark) and thousands of weekly npm downloads.
|
||||||
|
|
||||||
|
Like Radix Primitives and React Aria, Ark UI allows you to style headless components using any method you prefer, including CSS, Tailwind CSS, Panda CSS, or Styled Components. One of its distinguishing features is multi-framework support.
|
||||||
|
|
||||||
|
### Installing and using Ark UI
|
||||||
|
|
||||||
|
In this example, we’ll build another dialog box using Ark UI and style it with vanilla CSS.
|
||||||
|
|
||||||
|
First, create a new React project or open an existing one. Then install Ark UI for React:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @ark-ui/react
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, import and use the unstyled components. Below is the anatomy of a dialog in Ark UI:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ArkDialog.jsx
|
||||||
|
|
||||||
|
import { Dialog, Portal } from '@ark-ui/react';
|
||||||
|
import './ark.style.css';
|
||||||
|
|
||||||
|
function ArkDialog() {
|
||||||
|
return (
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger className='btn primary-btn'>
|
||||||
|
Ark UI Dialog
|
||||||
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
<Portal>
|
||||||
|
<Dialog.Backdrop />
|
||||||
|
|
||||||
|
<Dialog.Positioner>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Title>
|
||||||
|
Confirm Deletion
|
||||||
|
</Dialog.Title>
|
||||||
|
|
||||||
|
<Dialog.Description>
|
||||||
|
Are you sure you want to permanently delete this file?
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<div className='bottom-btns'>
|
||||||
|
<Dialog.CloseTrigger className='btn'>
|
||||||
|
Cancel
|
||||||
|
</Dialog.CloseTrigger>
|
||||||
|
|
||||||
|
<Dialog.CloseTrigger className='btn red-btn'>
|
||||||
|
Delete Forever
|
||||||
|
</Dialog.CloseTrigger>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArkDialog;
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, style the component using your preferred method. Here is a vanilla CSS example:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* ark.style.css */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1.2rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: #1e64e7;
|
||||||
|
color: white;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-btn {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope="dialog"][data-part="backdrop"] {
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
animation: backdropAnimation 200ms cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope="dialog"][data-part="positioner"] {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
translate: -50% -50%;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope="dialog"][data-part="content"] {
|
||||||
|
background-color: white;
|
||||||
|
padding: 2.5rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
|
||||||
|
rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope="dialog"][data-part="title"] {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 3px solid #dfdddd;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope="dialog"][data-part="description"] {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-btns .btn:last-child {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes backdropAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, export and render the component. Below is the output of the example:
|
||||||
|
|
||||||
|
Dialog component built using Ark UI styled with custom CSS
|
||||||
|
|
||||||
|
### Ark UI pros and cons
|
||||||
|
|
||||||
|
**Pros**
|
||||||
|
|
||||||
|
- It provides 34 main components.
|
||||||
|
- It includes advanced components such as a carousel and circular progress bar, which can be complex to implement from scratch.
|
||||||
|
- It supports [component composition](https://ark-ui.com/react/docs/guides/composition) using the `asChild` prop, similar to Radix Primitives.
|
||||||
|
|
||||||
|
**Cons**
|
||||||
|
|
||||||
|
- It does not provide built-in class names like React Aria.
|
||||||
|
- The recommended styling approach relies on `data-scope` and `data-part` attributes, which may feel unfamiliar at first.
|
||||||
|
|
||||||
|
For example, styling a specific part of the dialog can look like this:
|
||||||
|
|
||||||
|
```css
|
||||||
|
[data-scope="dialog"][data-part="positioner"] {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
translate: -50% -50%;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Developers who prefer a more familiar workflow can assign custom class names using `className` and target those instead:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.primary-btn {
|
||||||
|
background-color: #1e64e7;
|
||||||
|
color: white;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach preserves Ark UI’s headless behavior while allowing conventional CSS styling.
|
||||||
|
|
||||||
|
## Base UI
|
||||||
|
|
||||||
|
[Base UI](https://base-ui.com/) is a library of unstyled React components built by contributors from Radix, Floating UI, and the Material UI team. While it follows the same headless philosophy as the other libraries discussed in this article, Base UI places a stronger emphasis on stable APIs that are well-suited for building long-term custom design systems. At the time of writing, Base UI has more than 8.1K stars on its [GitHub repository](https://github.com/mui/base-ui) and is actively maintained with regular releases.
|
||||||
|
|
||||||
|
Like the other headless libraries in this guide, Base UI components can be styled using CSS, Tailwind CSS, or CSS-in-JS. The documentation also includes guidance on advanced patterns such as controlled dialogs and detached triggers.
|
||||||
|
|
||||||
|
### Installing and using Base UI
|
||||||
|
|
||||||
|
Unlike Radix Primitives, which publishes each component separately, Base UI ships all components in a single tree-shakable package. This makes installation straightforward.
|
||||||
|
|
||||||
|
First, create a new React project or open an existing one. Then install Base UI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @base-ui/react
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, create a file and import the `Dialog` component. In this example, we’ll build another dialog box:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// BaseDialog.jsx
|
||||||
|
|
||||||
|
import { Dialog } from '@base-ui/react/dialog';
|
||||||
|
import './base.style.css';
|
||||||
|
|
||||||
|
function BaseDialog() {
|
||||||
|
return (
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger className='btn primary-btn'>
|
||||||
|
Base UI Dialog
|
||||||
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Backdrop className='dialog-overlay' />
|
||||||
|
|
||||||
|
<Dialog.Popup className='dialog-content'>
|
||||||
|
<Dialog.Title className='dialog-title'>
|
||||||
|
Confirm Deletion
|
||||||
|
</Dialog.Title>
|
||||||
|
|
||||||
|
<Dialog.Description className='dialog-body'>
|
||||||
|
Are you sure you want to permanently delete this file?
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<div className='bottom-btns'>
|
||||||
|
<Dialog.Close className='btn'>
|
||||||
|
Cancel
|
||||||
|
</Dialog.Close>
|
||||||
|
|
||||||
|
<Dialog.Close className='btn red-btn'>
|
||||||
|
Delete Forever
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</Dialog.Popup>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseDialog;
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, add styling:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* base.style.css */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1.2rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: #1e64e7;
|
||||||
|
color: white;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-btn {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-overlay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
animation: overlayAnimation 200ms cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
background-color: white;
|
||||||
|
position: fixed;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
translate: -50% -50%;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 450px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
|
||||||
|
rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 3px solid #dfdddd;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-btns .btn:last-child {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes overlayAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, import and render the component in your application:
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import './App.css';
|
||||||
|
import BaseDialog from './BaseDialog';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseDialog />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
```
|
||||||
|
|
||||||
|
And you should see output similar to the example below:
|
||||||
|
|
||||||
|
Dialog component built using Base UI styled with custom CSS
|
||||||
|
|
||||||
|
### Base UI pros and cons
|
||||||
|
|
||||||
|
**Pros**
|
||||||
|
|
||||||
|
- It ships as a single tree-shakable package, eliminating the need to install components individually.
|
||||||
|
- It includes strong documentation and supports advanced patterns such as controlled dialogs and detached triggers.
|
||||||
|
|
||||||
|
**Cons**
|
||||||
|
|
||||||
|
- Its ecosystem is still growing compared to more established alternatives.
|
||||||
|
- Because it is unstyled by design, significant styling work is still required to align it with a production design system.
|
||||||
|
|
||||||
|
## Comparing the headless component libraries
|
||||||
|
|
||||||
|
To provide a clearer overview of how these headless UI libraries compare across API design, styling flexibility, composition model, and intended use cases, the table below highlights the key differences between Radix Primitives, React Aria, Ark UI, and Base UI.
|
||||||
|
|
||||||
|
| Dimension | Radix Primitives | React Aria | Ark UI | Base UI |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| **Primary goal** | Polished primitives for app UIs | Accessibility-first primitives | Cross-framework state-driven primitives | Foundation for custom design systems |
|
||||||
|
| **Mental model** | Component anatomy and composition | Hooks with explicit state | State machines and parts | Low-level primitives meant to be wrapped |
|
||||||
|
| **Typical usage** | Used directly in application code | Composed per component | Assembled from parts | Extended into internal components |
|
||||||
|
| **Styling approach** | `className`, `asChild` | Built-in classes with overrides | `data-part` / `data-scope` with `className` | `className` and wrapper components |
|
||||||
|
| **Ease of styling** | Easy and familiar | Easy once conventions are understood | Moderate, unconventional at first | Easy, but assumes design ownership |
|
||||||
|
| **Composition flexibility** | High | Very high | High | Very high |
|
||||||
|
| **Accessibility transparency** | Mostly abstracted | Very explicit | Abstracted via state | Abstracted but predictable |
|
||||||
|
| **Learning curve** | Moderate | Steep | Moderate to steep | Moderate |
|
||||||
|
| **Best suited for** | Product teams building applications | Accessibility-critical applications | Multi-framework design systems | Teams building custom design systems |
|
||||||
|
| **Framework support** | React | React | React, Vue, Solid | React |
|
||||||
|
|
||||||
|
This comparison demonstrates that while these libraries often provide similar component coverage, they differ significantly in how components are composed, styled, and extended.
|
||||||
|
|
||||||
|
Choosing the right headless UI library ultimately depends on your project goals, team preferences, and long-term maintenance strategy. The following quick guide can help narrow down your options:
|
||||||
|
|
||||||
|
- **Use Radix Primitives** if you want mature, well-documented components that can be used directly in application code with minimal setup.
|
||||||
|
- **Use React Aria** if accessibility is a primary concern and you prefer explicit, hook-based control over component behavior.
|
||||||
|
- **Use Ark UI** if you need headless components that work across multiple frameworks such as React, Vue, and Solid.
|
||||||
|
- **Use Base UI** if you are building a custom design system and want a flexible, long-term foundation for your own components.
|
||||||
|
|
||||||
|
The best choice depends less on feature parity and more on how well a library’s design philosophy aligns with your team’s workflow and architectural goals.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This guide explored why developers may look beyond Tailwind Labs’ Headless UI library when choosing unstyled component libraries. We examined several strong alternatives, including Radix Primitives, React Aria, Ark UI, and Base UI.
|
||||||
|
|
||||||
|
The frontend ecosystem continues to adopt headless UI libraries because many teams want more control over how components behave and how they are styled. Having multiple headless options available is beneficial, as different projects have different architectural and design needs.
|
||||||
|
|
||||||
|
## Get set up with LogRocket's modern React error tracking in minutes:
|
||||||
|
|
||||||
|
1. Visit [https://logrocket.com/signup/](https://lp.logrocket.com/blg/react-signup-general) to get
|
||||||
|
an app ID
|
||||||
|
|
||||||
|
2. Install LogRocket via npm or script tag. `LogRocket.init()` must be called client-side, not
|
||||||
|
server-side
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- [npm](https://blog.logrocket.com/headless-ui-alternatives/#plug-tab-1)
|
||||||
|
- [Script tag](https://blog.logrocket.com/headless-ui-alternatives/#plug-tab-2)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm i --save logrocket
|
||||||
|
|
||||||
|
// Code:
|
||||||
|
|
||||||
|
import LogRocket from 'logrocket';
|
||||||
|
LogRocket.init('app/id');
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
// Add to your HTML:
|
||||||
|
|
||||||
|
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
|
||||||
|
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
3. (Optional) Install plugins for deeper integrations with your stack:
|
||||||
|
- Redux middleware
|
||||||
|
|
||||||
|
- NgRx middleware
|
||||||
|
|
||||||
|
- Vuex plugin
|
||||||
|
|
||||||
|
[Get started now](https://lp.logrocket.com/blg/react-signup-general)
|
||||||
|
|
||||||
|
- [#react](https://blog.logrocket.com/tag/react/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Stop guessing about your digital experience with LogRocket
|
||||||
|
|
||||||
|
[Get started for free](https://lp.logrocket.com/blg/signup)
|
||||||
|
|
||||||
|
#### Recent posts:
|
||||||
|
|
||||||
|
[\\
|
||||||
|
**CRUD REST API with Node.js, Express, and PostgreSQL**](https://blog.logrocket.com/crud-rest-api-node-js-express-postgresql/)
|
||||||
|
|
||||||
|
Build a CRUD REST API with Node.js, Express, and PostgreSQL, then modernize it with ES modules, async/await, built-in Express middleware, and safer config handling.
|
||||||
|
|
||||||
|
[](https://blog.logrocket.com/author/taniarascia/)[Tania Rascia](https://blog.logrocket.com/author/taniarascia/)
|
||||||
|
|
||||||
|
Mar 25, 2026 ⋅ 16 min read
|
||||||
|
|
||||||
|
[\\
|
||||||
|
**The Replay (3/25/26): Senior dev hiring woes, what AI agents miss, and more**](https://blog.logrocket.com/the-replay-3-25-26/)
|
||||||
|
|
||||||
|
Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the March 25th issue.
|
||||||
|
|
||||||
|
[](https://blog.logrocket.com/author/matthewmaccormack/)[Matt MacCormack](https://blog.logrocket.com/author/matthewmaccormack/)
|
||||||
|
|
||||||
|
Mar 25, 2026 ⋅ 29 sec read
|
||||||
|
|
||||||
|
[\\
|
||||||
|
**The hidden skills gap in senior dev hiring (and how to screen for it)**](https://blog.logrocket.com/the-hidden-skills-gap-in-senior-dev-hiring/)
|
||||||
|
|
||||||
|
Discover a practical framework for redesigning your senior developer hiring process to screen for real diagnostic skill.
|
||||||
|
|
||||||
|
[](https://blog.logrocket.com/author/emmanueljohn/)[Emmanuel John](https://blog.logrocket.com/author/emmanueljohn/)
|
||||||
|
|
||||||
|
Mar 25, 2026 ⋅ 12 min read
|
||||||
|
|
||||||
|
[\\
|
||||||
|
**Does the Speculation Rules API boost web speed? I tested it**](https://blog.logrocket.com/speculation-rules-api-web-speed-test/)
|
||||||
|
|
||||||
|
I tested the Speculation Rules API in a real project to see if it actually improves navigation speed. Here’s what worked, what didn’t, and where it’s worth using.
|
||||||
|
|
||||||
|
[](https://blog.logrocket.com/author/judemiracle/)[Jude Miracle](https://blog.logrocket.com/author/judemiracle/)
|
||||||
|
|
||||||
|
Mar 24, 2026 ⋅ 10 min read
|
||||||
|
|
||||||
|
[View all posts](https://blog.logrocket.com/)
|
||||||
|
|
||||||
|
#### 2 Replies to "Headless UI alternatives: Radix Primitives vs. React Aria vs. Ark UI vs. Base UI"
|
||||||
|
|
||||||
|
1. \> such as Base UI (from the Material UI team)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
This is confusing. The link points to “MUI Base”. But this project has a successor now: Base UI, [https://base-ui.com/](https://base-ui.com/).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[Reply](https://blog.logrocket.com/headless-ui-alternatives/#comment-51239)
|
||||||
|
|
||||||
|
1. Thanks for noticing this. Should be all set now
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[Reply](https://blog.logrocket.com/headless-ui-alternatives/#comment-51257)
|
||||||
|
|
||||||
|
### Leave a Reply [Cancel reply](https://blog.logrocket.com/headless-ui-alternatives/\#respond)
|
||||||
|
|
||||||
|
Your email address will not be published.Required fields are marked \*
|
||||||
|
|
||||||
|
Comment \*
|
||||||
|
|
||||||
|
Name \*
|
||||||
|
|
||||||
|
Email \*
|
||||||
|
|
||||||
|
Website
|
||||||
|
|
||||||
|
Save my name, email, and website in this browser for the next time I comment.
|
||||||
|
|
||||||
|
Hey there, want to help make our blog better?
|
||||||
|
|
||||||
|
YeaNo Thanks
|
||||||
|
|
||||||
|
Join LogRocket’s Content Advisory Board. You’ll help inform the type of
|
||||||
|
content we create and get access to exclusive meetups, social accreditation,
|
||||||
|
and swag.
|
||||||
|
|
||||||
|
|
||||||
|
[Sign up now](https://lp.logrocket.com/blg/content-advisory-board-signup)
|
||||||
130
.firecrawl/openui-overview.md
Normal file
130
.firecrawl/openui-overview.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# Overview
|
||||||
|
|
||||||
|
Key building blocks of the OpenUI framework and the built-in component libraries.
|
||||||
|
|
||||||
|
Copy MarkdownOpen
|
||||||
|
|
||||||
|
OpenUI is built around four core building blocks that work together to turn LLM output into rendered UI:
|
||||||
|
|
||||||
|
- **Library** — A collection of components defined with Zod schemas and React renderers. The library is the contract between your app and the AI — it defines what components the LLM can use and how they render.
|
||||||
|
|
||||||
|
- **Prompt Generator** — Converts your library into a system prompt that instructs the LLM to output valid OpenUI Lang. Includes syntax rules, component signatures, streaming guidelines, and your custom examples/rules.
|
||||||
|
|
||||||
|
- **Parser** — Parses OpenUI Lang text (line-by-line, streaming-compatible) into a typed element tree. Validates against your library's JSON Schema and gracefully handles partial/invalid output.
|
||||||
|
|
||||||
|
- **Renderer** — The `<Renderer />` React component takes parsed output and maps each element to your library's React components, rendering the UI progressively as the stream arrives.
|
||||||
|
|
||||||
|
|
||||||
|
## [Built-in Component Libraries](https://www.openui.com/docs/openui-lang/overview\#built-in-component-libraries)
|
||||||
|
|
||||||
|
OpenUI ships with two ready-to-use libraries via `@openuidev/react-ui`. Both include layouts, content blocks, charts, forms, tables, and more.
|
||||||
|
|
||||||
|
### [General-purpose library (`openuiLibrary`)](https://www.openui.com/docs/openui-lang/overview\#general-purpose-library-openuilibrary)
|
||||||
|
|
||||||
|
Root component is `Stack`. Includes the full component suite with flexible layout primitives. Use this for standalone rendering, playgrounds, and non-chat interfaces.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib";
|
||||||
|
import { Renderer } from "@openuidev/react-lang";
|
||||||
|
|
||||||
|
// Generate system prompt
|
||||||
|
const systemPrompt = openuiLibrary.prompt(openuiPromptOptions);
|
||||||
|
|
||||||
|
// Render streamed output
|
||||||
|
<Renderer library={openuiLibrary} response={streamedText} isStreaming={isStreaming} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Chat-optimized library (`openuiChatLibrary`)](https://www.openui.com/docs/openui-lang/overview\#chat-optimized-library-openuichatlibrary)
|
||||||
|
|
||||||
|
Root component is `Card` (vertical container, no layout params). Adds chat-specific components like `FollowUpBlock`, `ListBlock`, and `SectionBlock`. Does not include `Stack` — responses are always single-card, vertically stacked.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { openuiChatLibrary, openuiChatPromptOptions } from "@openuidev/react-ui/genui-lib";
|
||||||
|
import { FullScreen } from "@openuidev/react-ui";
|
||||||
|
|
||||||
|
// Use with a chat layout
|
||||||
|
<FullScreen
|
||||||
|
componentLibrary={openuiChatLibrary}
|
||||||
|
processMessage={...}
|
||||||
|
streamProtocol={openAIAdapter()}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Both libraries expose a `.prompt()` method to generate the system prompt your LLM needs. See [System Prompts](https://www.openui.com/docs/openui-lang/system-prompts) for CLI and programmatic generation options.
|
||||||
|
|
||||||
|
### [Extend a built-in library](https://www.openui.com/docs/openui-lang/overview\#extend-a-built-in-library)
|
||||||
|
|
||||||
|
```
|
||||||
|
import { createLibrary, defineComponent } from "@openuidev/react-lang";
|
||||||
|
import { openuiLibrary } from "@openuidev/react-ui/genui-lib";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const ProductCard = defineComponent({
|
||||||
|
name: "ProductCard",
|
||||||
|
description: "Product tile",
|
||||||
|
props: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
price: z.number(),
|
||||||
|
}),
|
||||||
|
component: ({ props }) => <div>{props.name}: ${props.price}</div>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const myLibrary = createLibrary({
|
||||||
|
root: openuiLibrary.root ?? "Stack",
|
||||||
|
componentGroups: openuiLibrary.componentGroups,
|
||||||
|
components: [...Object.values(openuiLibrary.components), ProductCard],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## [Usage Example](https://www.openui.com/docs/openui-lang/overview\#usage-example)
|
||||||
|
|
||||||
|
Define LibRender CodeSystem PromptLLM Output
|
||||||
|
|
||||||
|
OpenUI Lang (Token Efficient)Copy
|
||||||
|
|
||||||
|
```
|
||||||
|
root = Stack([welcomeCard])
|
||||||
|
welcomeCard = MyCard([welcomeHeader, welcomeBody])
|
||||||
|
welcomeHeader = CardHeader("Welcome", "Get started with our platform")
|
||||||
|
welcomeBody = Stack([signupForm], "column", "m")
|
||||||
|
signupForm = Form("signup", [nameField, emailField], actions)
|
||||||
|
nameField = FormControl("Name", Input("name", "Your name", "text", ["required", "minLength:2"]))
|
||||||
|
emailField = FormControl("Email", Input("email", "you@example.com", "email", ["required", "email"]))
|
||||||
|
actions = Buttons([signUpBtn, learnMoreBtn], "row")
|
||||||
|
signUpBtn = Button("Sign up", "submit:signup", "primary")
|
||||||
|
learnMoreBtn = Button("Learn more", "action:learn_more", "secondary")
|
||||||
|
```
|
||||||
|
|
||||||
|
Output Preview
|
||||||
|
|
||||||
|
Welcome
|
||||||
|
|
||||||
|
Get started with our platform
|
||||||
|
|
||||||
|
Name\*
|
||||||
|
|
||||||
|
Email\*
|
||||||
|
|
||||||
|
Sign upLearn more
|
||||||
|
|
||||||
|
## [Next Steps](https://www.openui.com/docs/openui-lang/overview\#next-steps)
|
||||||
|
|
||||||
|
[**Defining Components** \\
|
||||||
|
\\
|
||||||
|
Create custom components with Zod schemas and React renderers.](https://www.openui.com/docs/openui-lang/defining-components) [**System Prompts** \\
|
||||||
|
\\
|
||||||
|
Generate and customize LLM instructions from your library.](https://www.openui.com/docs/openui-lang/system-prompts) [**The Renderer** \\
|
||||||
|
\\
|
||||||
|
Parse and render streamed OpenUI Lang in React.](https://www.openui.com/docs/openui-lang/renderer) [**Chat Integration** \\
|
||||||
|
\\
|
||||||
|
Build AI chat interfaces with prebuilt layouts.](https://www.openui.com/docs/chat)
|
||||||
|
|
||||||
|
[Quick Start\\
|
||||||
|
\\
|
||||||
|
Bootstrap a Generative UI chat app in under a minute.](https://www.openui.com/docs/openui-lang/quickstart) [Defining Components\\
|
||||||
|
\\
|
||||||
|
Define OpenUI components with Zod and React renderers.](https://www.openui.com/docs/openui-lang/defining-components)
|
||||||
|
|
||||||
|
### On this page
|
||||||
|
|
||||||
|
[Built-in Component Libraries](https://www.openui.com/docs/openui-lang/overview#built-in-component-libraries) [General-purpose library (`openuiLibrary`)](https://www.openui.com/docs/openui-lang/overview#general-purpose-library-openuilibrary) [Chat-optimized library (`openuiChatLibrary`)](https://www.openui.com/docs/openui-lang/overview#chat-optimized-library-openuichatlibrary) [Extend a built-in library](https://www.openui.com/docs/openui-lang/overview#extend-a-built-in-library) [Usage Example](https://www.openui.com/docs/openui-lang/overview#usage-example) [Next Steps](https://www.openui.com/docs/openui-lang/overview#next-steps)
|
||||||
289
.firecrawl/openui-spec.md
Normal file
289
.firecrawl/openui-spec.md
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
# OpenUI Specification
|
||||||
|
|
||||||
|
draft-01
|
||||||
|
|
||||||
|
## 1\. Introduction
|
||||||
|
|
||||||
|
**OpenUI** is a specification format for defining User Interface (UI) components in an abstract, implementation-agnostic manner. Inspired by specifications like **OpenAPI**, OpenUI describes UI components, their properties and behaviors in a way that can be used across various UI libraries and frameworks. This approach provides a standardized, machine-readable and human-readable model for UI libraries, facilitating a consistent means of documentation, testing and code generation.
|
||||||
|
|
||||||
|
OpenUI is designed to be **AI-native**, making it easier for AI tools and assistants to parse, understand and leverage information about your UI components or design system. By offering a uniform description of components, OpenUI aids in bridging the gap between diverse frameworks, ensuring interoperability and reducing fragmentation.
|
||||||
|
|
||||||
|
## 2\. Specification Interpretation (RFC 2119)
|
||||||
|
|
||||||
|
This specification uses key terms from [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119):
|
||||||
|
|
||||||
|
- **MUST** indicates an absolute requirement.
|
||||||
|
- **SHOULD** indicates a recommendation that, while not strictly required, carries significant weight if not implemented.
|
||||||
|
- **MAY** indicates an option or possibility left to implementer discretion.
|
||||||
|
|
||||||
|
These terms are used carefully within this document to clarify how certain aspects of OpenUI **MUST**, **SHOULD**, or **MAY** be interpreted or implemented.
|
||||||
|
|
||||||
|
## 3\. Purpose and Goals
|
||||||
|
|
||||||
|
OpenUI addresses the growing need for a common language to describe UI components. It streamlines collaboration among designers, developers, technical writers and AI-based systems. Its key goals are:
|
||||||
|
|
||||||
|
1. **Standardizing UI Component Libraries**
|
||||||
|
|
||||||
|
|
||||||
|
Provide a consistent schema for describing components and design systems.
|
||||||
|
|
||||||
|
2. **Interoperability Across Frameworks**
|
||||||
|
|
||||||
|
|
||||||
|
Abstract away implementation details, enabling easier migration or shared usage across React, Vue, Angular and other libraries.
|
||||||
|
|
||||||
|
3. **AI-Native**
|
||||||
|
|
||||||
|
|
||||||
|
Supply structured data optimized for consumption by AI tools, improving automated documentation, validation and code generation.
|
||||||
|
|
||||||
|
4. **Efficiency**
|
||||||
|
|
||||||
|
|
||||||
|
Reduce duplication and confusion by having a single, authoritative reference that clarifies how components behave and interact.
|
||||||
|
|
||||||
|
5. **Built-In Accessibility and Validation**
|
||||||
|
|
||||||
|
|
||||||
|
Promote best practices for accessible design while enabling validation and testing tools to verify component conformance.
|
||||||
|
|
||||||
|
6. **Support for Web and Native Platforms**
|
||||||
|
|
||||||
|
|
||||||
|
Offer a universal approach that can adapt to both web-based and native environments.
|
||||||
|
|
||||||
|
7. **Documentation + Testing + Code Generation**
|
||||||
|
|
||||||
|
|
||||||
|
Enable a broad ecosystem of tooling, from auto-generated docs to integrated testing frameworks.
|
||||||
|
|
||||||
|
|
||||||
|
## 4\. Overview and Key Features
|
||||||
|
|
||||||
|
### AI-Native Specification
|
||||||
|
|
||||||
|
OpenUI's structured format is particularly well-suited for AI-based analysis and tooling. By adhering to a consistent schema, developers can integrate OpenUI with LLM-based assistants or other AI tools to:
|
||||||
|
|
||||||
|
- Generate automated tests and documentation.
|
||||||
|
- Provide contextual suggestions during development.
|
||||||
|
- Perform higher-level reasoning about component usage.
|
||||||
|
|
||||||
|
### Standardization of UI Components
|
||||||
|
|
||||||
|
OpenUI makes it straightforward to define each component's shape, props, events and usage guidelines. This standardization can reduce the learning curve for new developers and makes it easier to share components across teams or projects.
|
||||||
|
|
||||||
|
### Efficiency and Universal Application
|
||||||
|
|
||||||
|
By referencing a single specification file, organizations can ensure consistent APIs and behaviors across multiple implementations. OpenUI also supports various popular UI libraries and frameworks out of the box.
|
||||||
|
|
||||||
|
## 5\. Similarities and Differences with Other Tools
|
||||||
|
|
||||||
|
1. **Type Definition Files**
|
||||||
|
- **Similarities**: Both OpenUI and typical type definition files (e.g., TypeScript `.d.ts`) can define types, enumerations and interfaces in a way that tools can parse.
|
||||||
|
- **Differences**:
|
||||||
|
|
||||||
|
- Type definitions often focus on compile-time checks and may include internal or unrelated types across multiple files, without extensive usage or behavioral documentation.
|
||||||
|
- OpenUI, by contrast, is designed to be more concise and self-contained. It places all relevant component information (including usage examples, accessibility considerations and enumerated props) into a single, portable file. This provides a broader but more focused view of each component’s intended purpose and functionality.
|
||||||
|
2. `**llms.txt**`
|
||||||
|
- **Similarities**: Can also include structured text or partial metadata for Large Language Models (LLMs).
|
||||||
|
- **Differences**: `llms.txt` often contains entire documentation sets or large text blocks, making them unwieldy for direct AI consumption without retrieval augmented generation (RAG). OpenUI, by contrast, is deliberately minimal and structured specifically for UI component definitions, easing both human and machine comprehension.
|
||||||
|
3. **Storybook**
|
||||||
|
- **Similarities**: Provides extensive documentation for UI components and their usage patterns.
|
||||||
|
- **Differences**: Storybook offers live previews and interactive exploration, while OpenUI is a static, implementation-agnostic specification.
|
||||||
|
4. **OpenAPI**
|
||||||
|
- **Similarities**: Both define a domain (OpenAPI for HTTP APIs, OpenUI for UI components) using a schema that is machine-readable and human-readable.
|
||||||
|
- **Differences**: OpenAPI is dedicated to endpoints and data payloads, whereas OpenUI focuses on user interface components and their properties.
|
||||||
|
|
||||||
|
OpenUI **complements** these existing tools by providing a formal specification for UI components, bridging the gap between full interactive environments (e.g., Storybook) and purely type-focused definitions (e.g., TypeScript `.d.ts` files).
|
||||||
|
|
||||||
|
## 6\. Why OpenUI?
|
||||||
|
|
||||||
|
1. **Consistency**
|
||||||
|
|
||||||
|
|
||||||
|
Streamlines how UI components are documented, reducing confusion among teams and projects.
|
||||||
|
|
||||||
|
2. **Framework Agnosticism**
|
||||||
|
|
||||||
|
|
||||||
|
Fits multiple frameworks (e.g., React, Vue, Angular) by focusing on essential component definitions rather than implementation details.
|
||||||
|
|
||||||
|
3. **Standardization**
|
||||||
|
|
||||||
|
|
||||||
|
Encourages best practices for design systems and shared UI libraries.
|
||||||
|
|
||||||
|
4. **Enhanced Developer Experience**
|
||||||
|
|
||||||
|
|
||||||
|
Simplifies onboarding and collaboration, as all information about a component is located in a centralized specification.
|
||||||
|
|
||||||
|
5. **AI Readiness**
|
||||||
|
|
||||||
|
|
||||||
|
Supplies metadata in a format that AI tools can easily parse, leading to improved automated code insights, error detection and overall development efficiency.
|
||||||
|
|
||||||
|
|
||||||
|
## 7\. Specification Format
|
||||||
|
|
||||||
|
OpenUI specifications **MUST** be defined in **YAML** or **JSON**. Below is the general structure required at the top level:
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **name** | string | Name of the UI library or component set. |
|
||||||
|
| **version** | string | Version of the UI library or component set. |
|
||||||
|
| **description** | string | Brief overview of the UI library or component set. |
|
||||||
|
| **components** | object | Collection of components in the library. |
|
||||||
|
|
||||||
|
Each entry in `components` represents a **component**, defined by:
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **package** | string (opt.) | (Optional) Package or module where the component is located. |
|
||||||
|
| **description** | string | Component overview. |
|
||||||
|
| **example** | string (opt.) | (Optional) Example usage of the component. |
|
||||||
|
| **props** | object | Map of properties supported by the component. |
|
||||||
|
|
||||||
|
Within `props`, each **prop** can be a string indicating the type or an object containing:
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **type** | string | Data type for the prop (e.g., `string`, `boolean`, `number`). |
|
||||||
|
| **description** | string | Explanation of the prop's purpose and usage. |
|
||||||
|
| **default** | any | Default value for the prop, if applicable. |
|
||||||
|
| **enum** | array | List of allowable values (if the prop is an enumerated type). |
|
||||||
|
| **required** | boolean | Whether the prop is mandatory (e.g., `true` means it MUST be provided). |
|
||||||
|
|
||||||
|
## 8\. Filename and Discovery
|
||||||
|
|
||||||
|
The OpenUI specification file **MUST** be named **`openui.yaml`** or **`openui.json`**. It **SHOULD** reside at the **root** of your GitHub repository or the base path of your website. This approach makes it straightforward for both humans and automated tools to locate and parse the specification.
|
||||||
|
|
||||||
|
## 9\. Example Specification
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Example UI Library
|
||||||
|
version: 1.0.0
|
||||||
|
description: A sample UI library specification
|
||||||
|
|
||||||
|
components:
|
||||||
|
Button:
|
||||||
|
description: A clickable button element
|
||||||
|
example: |
|
||||||
|
<Button variant="primary" disabled={false}>
|
||||||
|
Click Me
|
||||||
|
</Button>
|
||||||
|
props:
|
||||||
|
variant:
|
||||||
|
type: string
|
||||||
|
description: The visual style of the button.
|
||||||
|
enum:
|
||||||
|
- primary
|
||||||
|
- secondary
|
||||||
|
- outline
|
||||||
|
size:
|
||||||
|
type: string
|
||||||
|
description: The size of the button.
|
||||||
|
enum:
|
||||||
|
- small
|
||||||
|
- medium
|
||||||
|
- large
|
||||||
|
disabled:
|
||||||
|
type: boolean
|
||||||
|
description: Disables the button if set to true.
|
||||||
|
default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example:
|
||||||
|
|
||||||
|
- **`variant`** is a string-based enumerated prop.
|
||||||
|
- **`size`** is another enumerated prop.
|
||||||
|
- **`disabled`** is a boolean with a default value.
|
||||||
|
|
||||||
|
## 10\. Best Practices
|
||||||
|
|
||||||
|
1. **Reflect the Design System Accurately**
|
||||||
|
|
||||||
|
|
||||||
|
All components, props and enumerations **SHOULD** accurately match the functionality and constraints in the actual UI library or design system.
|
||||||
|
|
||||||
|
2. **Document Events**
|
||||||
|
|
||||||
|
|
||||||
|
If a component emits events (e.g., `onClick`), these **SHOULD** be included to ensure completeness.
|
||||||
|
|
||||||
|
3. **Emphasize Accessibility**
|
||||||
|
|
||||||
|
|
||||||
|
Integrate relevant accessibility attributes or guidelines within your component definitions to promote inclusive design.
|
||||||
|
|
||||||
|
4. **Maintain Consistency**
|
||||||
|
|
||||||
|
|
||||||
|
Use a uniform style for naming and describing components and props across the specification.
|
||||||
|
|
||||||
|
5. **Keep the Specification Updated**
|
||||||
|
|
||||||
|
|
||||||
|
Update the specification whenever the codebase evolves to avoid discrepancies.
|
||||||
|
|
||||||
|
|
||||||
|
## 11\. Future Plans
|
||||||
|
|
||||||
|
OpenUI is an evolving specification. Planned or potential enhancements include:
|
||||||
|
|
||||||
|
1. **Describing Component State and Lifecycle**
|
||||||
|
|
||||||
|
|
||||||
|
Support for advanced UI patterns, including transitions and complex state management.
|
||||||
|
|
||||||
|
2. **Tooling for Code Generation**
|
||||||
|
|
||||||
|
|
||||||
|
Official plugins or libraries to automatically generate documentation, tests and skeleton code.
|
||||||
|
|
||||||
|
3. **Library and Framework Integrations**
|
||||||
|
|
||||||
|
|
||||||
|
Core support for popular frameworks such as React, Vue, Angular and others.
|
||||||
|
|
||||||
|
4. **Complex UI Patterns**
|
||||||
|
|
||||||
|
|
||||||
|
Handling dynamic scenarios such as multi-step forms, modals, or asynchronous data loading.
|
||||||
|
|
||||||
|
|
||||||
|
## 12\. Contributing
|
||||||
|
|
||||||
|
OpenUI is an open-source project and contributions are welcomed. If you wish to propose changes or add new features:
|
||||||
|
|
||||||
|
1. **Fork the Repository**
|
||||||
|
|
||||||
|
|
||||||
|
Create a personal fork of the OpenUI repository.
|
||||||
|
|
||||||
|
2. **Create a Branch**
|
||||||
|
|
||||||
|
|
||||||
|
Use a descriptive name (e.g., `feature/add-modal-spec`).
|
||||||
|
|
||||||
|
3. **Implement Changes**
|
||||||
|
|
||||||
|
|
||||||
|
Follow the established style and schema guidelines when modifying or adding files.
|
||||||
|
|
||||||
|
4. **Validate**
|
||||||
|
|
||||||
|
|
||||||
|
Confirm your updates are syntactically correct and conform to the schema requirements.
|
||||||
|
|
||||||
|
5. **Submit a Pull Request**
|
||||||
|
|
||||||
|
|
||||||
|
Propose your changes for review by the maintainers.
|
||||||
|
|
||||||
|
|
||||||
|
For more information, refer to the [OpenUI repository](https://github.com/ctate/openui).
|
||||||
|
|
||||||
|
## 13\. License
|
||||||
|
|
||||||
|
The OpenUI specification is distributed under the **MIT License**
|
||||||
361
.firecrawl/pkgpulse-bundling.md
Normal file
361
.firecrawl/pkgpulse-bundling.md
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
[Skip to main content](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026#main-content)
|
||||||
|
|
||||||
|
## [TL;DR](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#tldr)
|
||||||
|
|
||||||
|
**tsup** is the most popular TypeScript library bundler — zero-config, generates ESM + CJS + `.d.ts` types automatically, used by thousands of npm packages. **tsdown** is the next-generation successor to tsup — built on Rolldown (Vite's Rust-based bundler), significantly faster, same DX but better performance. **unbuild** is from the UnJS ecosystem — supports multiple build presets, stub mode for development (no build step), and is used by Nuxt, Nitro, and UnJS libraries internally. In 2026: tsup is the safe choice with the largest community, tsdown is the emerging performance leader, unbuild if you're in the Nuxt/UnJS ecosystem.
|
||||||
|
|
||||||
|
## [Key Takeaways](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#key-takeaways)
|
||||||
|
|
||||||
|
- **tsup**: ~6M weekly downloads — esbuild-based, zero-config, ESM + CJS + types in one command
|
||||||
|
- **tsdown**: ~500K weekly downloads — Rolldown-based (Rust), 3-5x faster than tsup, tsup-compatible API
|
||||||
|
- **unbuild**: ~3M weekly downloads — UnJS, stub mode, multiple presets, Rollup-based
|
||||||
|
- All three generate dual ESM/CJS output — required for modern npm packages
|
||||||
|
- All three generate TypeScript declaration files (`.d.ts`) automatically
|
||||||
|
- tsdown is rapidly gaining adoption in 2026 as the fast tsup replacement
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Why Library Bundling Matters](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#why-library-bundling-matters)
|
||||||
|
|
||||||
|
```
|
||||||
|
Problem: publishing TypeScript to npm requires:
|
||||||
|
1. Compile TypeScript → JavaScript
|
||||||
|
2. Generate .d.ts type declarations
|
||||||
|
3. Create both ESM (import) and CJS (require) versions
|
||||||
|
4. Tree-shaking to minimize bundle size
|
||||||
|
5. Correct package.json "exports" map
|
||||||
|
|
||||||
|
Without a bundler:
|
||||||
|
tsc --outDir dist → CJS only, no bundling
|
||||||
|
+ manually maintain package.json exports
|
||||||
|
+ separately configure dts generation
|
||||||
|
+ no tree-shaking
|
||||||
|
|
||||||
|
With tsup/tsdown/unbuild:
|
||||||
|
One command → dist/index.mjs + dist/index.cjs + dist/index.d.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [tsup](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#tsup)
|
||||||
|
|
||||||
|
[tsup](https://tsup.egoist.dev/) — zero-config library bundler:
|
||||||
|
|
||||||
|
### [Setup](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#setup)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D tsup typescript
|
||||||
|
|
||||||
|
# Add to package.json scripts:
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [tsup.config.ts](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#tsupconfigts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from "tsup"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/index.ts"], // Entry point(s)
|
||||||
|
format: ["esm", "cjs"], // Output ESM + CommonJS
|
||||||
|
dts: true, // Generate .d.ts files
|
||||||
|
splitting: false, // Code splitting (for multiple entry points)
|
||||||
|
sourcemap: true, // Generate source maps
|
||||||
|
clean: true, // Clean dist/ before build
|
||||||
|
minify: false, // Don't minify libraries (let consumers decide)
|
||||||
|
external: ["react", "vue"], // Don't bundle peer dependencies
|
||||||
|
treeshake: true, // Remove unused code
|
||||||
|
target: "es2020", // Output target
|
||||||
|
outDir: "dist",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Multiple entry points](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#multiple-entry-points)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from "tsup"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// Multiple entry points (for sub-path exports):
|
||||||
|
entry: {
|
||||||
|
index: "src/index.ts",
|
||||||
|
server: "src/server.ts",
|
||||||
|
client: "src/client.ts",
|
||||||
|
},
|
||||||
|
format: ["esm", "cjs"],
|
||||||
|
dts: true,
|
||||||
|
splitting: true, // Share code between entry points
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generates:
|
||||||
|
// dist/index.mjs + dist/index.js + dist/index.d.ts
|
||||||
|
// dist/server.mjs + dist/server.js + dist/server.d.ts
|
||||||
|
// dist/client.mjs + dist/client.js + dist/client.d.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### [package.json for dual ESM/CJS](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#packagejson-for-dual-esmcjs)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-library",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "./dist/index.js", // CJS entry (legacy)
|
||||||
|
"module": "./dist/index.mjs", // ESM entry (bundlers)
|
||||||
|
"types": "./dist/index.d.ts", // TypeScript types
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/index.d.mts",
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"./server": {
|
||||||
|
"import": "./dist/server.mjs",
|
||||||
|
"require": "./dist/server.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": ["dist"],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsup": "^8.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Watch mode for development](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#watch-mode-for-development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rebuild on file changes:
|
||||||
|
tsup --watch
|
||||||
|
|
||||||
|
# Or in parallel with your dev server:
|
||||||
|
# package.json:
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently \"tsup --watch\" \"node dist/index.js\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [tsdown](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#tsdown)
|
||||||
|
|
||||||
|
[tsdown](https://tsdown.egoist.dev/) — the Rolldown-based tsup successor:
|
||||||
|
|
||||||
|
### [Why tsdown is faster](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#why-tsdown-is-faster)
|
||||||
|
|
||||||
|
```
|
||||||
|
tsup uses: esbuild (Go) → fast, but JS orchestration overhead
|
||||||
|
tsdown uses: Rolldown (Rust) → faster bundler + faster orchestration
|
||||||
|
|
||||||
|
Build time comparison (real-world library with 50 files):
|
||||||
|
tsup: ~2.5s
|
||||||
|
tsdown: ~0.6s
|
||||||
|
(varies by project size and machine)
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Setup (nearly identical to tsup)](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#setup-nearly-identical-to-tsup)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tsdown.config.ts
|
||||||
|
import { defineConfig } from "tsdown"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/index.ts"],
|
||||||
|
format: ["esm", "cjs"],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
sourcemap: true,
|
||||||
|
external: ["react"],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Commands are the same as tsup:
|
||||||
|
npx tsdown # Build
|
||||||
|
npx tsdown --watch # Watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
### [tsup → tsdown migration](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#tsup--tsdown-migration)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install:
|
||||||
|
npm uninstall tsup
|
||||||
|
npm install -D tsdown
|
||||||
|
|
||||||
|
# Rename config file:
|
||||||
|
mv tsup.config.ts tsdown.config.ts
|
||||||
|
|
||||||
|
# Update import:
|
||||||
|
# - import { defineConfig } from "tsup"
|
||||||
|
# + import { defineConfig } from "tsdown"
|
||||||
|
|
||||||
|
# Update package.json scripts:
|
||||||
|
# - "build": "tsup"
|
||||||
|
# + "build": "tsdown"
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [unbuild](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#unbuild)
|
||||||
|
|
||||||
|
[unbuild](https://github.com/unjs/unbuild) — UnJS library bundler:
|
||||||
|
|
||||||
|
### [Setup](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#setup-1)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// build.config.ts
|
||||||
|
import { defineBuildConfig } from "unbuild"
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
entries: ["src/index"],
|
||||||
|
rollup: {
|
||||||
|
emitCJS: true, // Also emit CommonJS
|
||||||
|
},
|
||||||
|
declaration: true, // Generate .d.ts
|
||||||
|
clean: true,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build:
|
||||||
|
npx unbuild
|
||||||
|
|
||||||
|
# Stub mode (development):
|
||||||
|
npx unbuild --stub
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Stub mode (unique to unbuild)](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#stub-mode-unique-to-unbuild)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// "Stub mode" — generates proxy files that require/import the source directly
|
||||||
|
// No build step needed during development!
|
||||||
|
|
||||||
|
// dist/index.mjs (stub):
|
||||||
|
// export * from "../src/index.ts"
|
||||||
|
|
||||||
|
// dist/index.js (stub):
|
||||||
|
// module.exports = require("../src/index.ts") // via jiti
|
||||||
|
|
||||||
|
// Benefits:
|
||||||
|
// - No watch mode needed — file changes are picked up immediately
|
||||||
|
// - Faster feedback loop when developing a library locally
|
||||||
|
// - Link the package to a consumer with npm link — changes are live
|
||||||
|
|
||||||
|
// Production build:
|
||||||
|
// npx unbuild ← produces real bundles (no stub)
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Multiple presets](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#multiple-presets)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineBuildConfig } from "unbuild"
|
||||||
|
|
||||||
|
export default defineBuildConfig([\
|
||||||
|
// Main package:\
|
||||||
|
{\
|
||||||
|
entries: ["src/index"],\
|
||||||
|
declaration: true,\
|
||||||
|
rollup: { emitCJS: true },\
|
||||||
|
},\
|
||||||
|
// CLI entry (no types needed):\
|
||||||
|
{\
|
||||||
|
entries: [{ input: "src/cli", name: "cli" }],\
|
||||||
|
declaration: false,\
|
||||||
|
rollup: {\
|
||||||
|
emitCJS: false,\
|
||||||
|
inlineDependencies: true, // Bundle everything into the CLI binary\
|
||||||
|
},\
|
||||||
|
},\
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Used by the UnJS ecosystem](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#used-by-the-unjs-ecosystem)
|
||||||
|
|
||||||
|
```
|
||||||
|
unbuild is used by:
|
||||||
|
- nuxt → @nuxt/... packages
|
||||||
|
- nitro → the Nuxt server engine
|
||||||
|
- h3 → the HTTP framework
|
||||||
|
- ofetch → the fetch wrapper
|
||||||
|
- Most @unjs/* packages
|
||||||
|
|
||||||
|
If you're contributing to or building in this ecosystem, unbuild
|
||||||
|
is the natural choice.
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Feature Comparison](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#feature-comparison)
|
||||||
|
|
||||||
|
| Feature | tsup | tsdown | unbuild |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Build engine | esbuild (Go) | Rolldown (Rust) | Rollup (JS) |
|
||||||
|
| Build speed | Fast | ⚡ Fastest | Moderate |
|
||||||
|
| ESM + CJS | ✅ | ✅ | ✅ |
|
||||||
|
| .d.ts generation | ✅ | ✅ | ✅ |
|
||||||
|
| Stub mode (no build) | ❌ | ❌ | ✅ |
|
||||||
|
| Code splitting | ✅ | ✅ | ✅ |
|
||||||
|
| treeshake | ✅ | ✅ | ✅ |
|
||||||
|
| Plugin ecosystem | esbuild plugins | Rolldown plugins | Rollup plugins |
|
||||||
|
| TypeScript config | tsup.config.ts | tsdown.config.ts | build.config.ts |
|
||||||
|
| Community size | ⭐ Large | Growing fast | Medium |
|
||||||
|
| Weekly downloads | ~6M | ~500K | ~3M |
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [When to Use Each](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#when-to-use-each)
|
||||||
|
|
||||||
|
**Choose tsup if:**
|
||||||
|
|
||||||
|
- The safe, battle-tested choice — most tutorials and examples use it
|
||||||
|
- Large community, most Stack Overflow answers, most plugins
|
||||||
|
- Works for 95% of library use cases out of the box
|
||||||
|
|
||||||
|
**Choose tsdown if:**
|
||||||
|
|
||||||
|
- Build speed is a priority (large libraries, frequent CI builds)
|
||||||
|
- You're migrating from tsup — API is nearly identical
|
||||||
|
- On the cutting edge of tooling in 2026
|
||||||
|
|
||||||
|
**Choose unbuild if:**
|
||||||
|
|
||||||
|
- Working in the Nuxt, Nitro, or UnJS ecosystem
|
||||||
|
- Want stub mode for instant development without watch rebuilds
|
||||||
|
- Need Rollup-specific plugins not available in esbuild/Rolldown
|
||||||
|
|
||||||
|
**Also consider:**
|
||||||
|
|
||||||
|
- **Vite Library Mode** — for libraries that need Vite plugins (CSS modules, etc.)
|
||||||
|
- **pkgroll** — minimal bundler for packages with simple needs
|
||||||
|
- **microbundle** — smaller alternative, but less actively maintained in 2026
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Methodology](https://www.pkgpulse.com/blog/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026\#methodology)
|
||||||
|
|
||||||
|
Download data from npm registry (weekly average, February 2026). Feature comparison based on tsup v8.x, tsdown v0.x, and unbuild v2.x.
|
||||||
|
|
||||||
|
_[Compare build tooling and bundler packages on PkgPulse →](https://www.pkgpulse.com/)_
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
### The 2026 JavaScript Stack Cheatsheet
|
||||||
|
|
||||||
|
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
|
||||||
|
|
||||||
|
Get the Free Cheatsheet
|
||||||
705
.firecrawl/pkgpulse-floating.md
Normal file
705
.firecrawl/pkgpulse-floating.md
Normal file
@ -0,0 +1,705 @@
|
|||||||
|
[Skip to main content](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026#main-content)
|
||||||
|
|
||||||
|
# [Floating UI vs Tippy.js vs Radix Tooltip: Popover Positioning 2026](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#floating-ui-vs-tippyjs-vs-radix-tooltip-popover-positioning-2026)
|
||||||
|
|
||||||
|
## [TL;DR](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#tldr)
|
||||||
|
|
||||||
|
Building tooltips, popovers, dropdowns, and floating menus correctly is deceptively hard — viewport overflow, collision detection, scroll containers, and keyboard accessibility are all gotchas that custom solutions routinely miss. **Floating UI** (successor to Popper.js from the same authors) is the low-level positioning engine — pure geometry and collision detection, totally unstyled, works with any framework, and is what Radix, Mantine, and many others use internally. **Tippy.js** is the batteries-included tooltip library built on Popper.js — styled out of the box, declarative API, animates, works in vanilla JS and React — but it's showing its age in 2026 with no App Router support and weaker accessibility guarantees. **Radix UI's Tooltip and Popover** are headless, fully accessible (WAI-ARIA compliant), React-only components built on Floating UI internally — the correct choice for React/Next.js component libraries where accessibility is non-negotiable. For low-level control over positioning in any framework: Floating UI. For quick tooltips with minimal config: Tippy.js. For production React UIs that must be accessible: Radix Tooltip/Popover.
|
||||||
|
|
||||||
|
## [Key Takeaways](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#key-takeaways)
|
||||||
|
|
||||||
|
- **Floating UI is framework-agnostic** — core is vanilla JS, `@floating-ui/react` adds React hooks
|
||||||
|
- **Floating UI handles all edge cases** — viewport overflow, flip, shift, arrow, virtual elements
|
||||||
|
- **Tippy.js is easiest to get started** — `<Tippy content="Tooltip">` wraps any element
|
||||||
|
- **Radix Tooltip is fully WAI-ARIA compliant** — focus management, screen readers, keyboard nav
|
||||||
|
- **Tippy.js is built on Popper.js** — Floating UI's predecessor, still maintained but less active
|
||||||
|
- **Radix Popover manages open state** — controlled and uncontrolled modes, portal rendering
|
||||||
|
- **Floating UI powers Radix internally** — Radix uses `@floating-ui/react-dom` under the hood
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Use Case Map](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#use-case-map)
|
||||||
|
|
||||||
|
```
|
||||||
|
Simple tooltip on hover → Tippy.js or Radix Tooltip
|
||||||
|
Tooltip with custom render → Floating UI or Radix Tooltip
|
||||||
|
Accessible popover with content → Radix Popover
|
||||||
|
Dropdown menu with keyboard nav → Radix DropdownMenu
|
||||||
|
Custom positioning engine → Floating UI (raw)
|
||||||
|
Framework-agnostic tooltip → Tippy.js or Floating UI
|
||||||
|
Select/Combobox overlay → Floating UI or Radix Select
|
||||||
|
Context menu (right-click) → Radix ContextMenu
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Floating UI: The Positioning Engine](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#floating-ui-the-positioning-engine)
|
||||||
|
|
||||||
|
Floating UI provides the geometry and collision detection algorithms — you wire up the DOM refs and React state yourself.
|
||||||
|
|
||||||
|
### [Installation](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#installation)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @floating-ui/react
|
||||||
|
# For vanilla JS (no React):
|
||||||
|
npm install @floating-ui/dom
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Basic Tooltip](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#basic-tooltip)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
useFloating,
|
||||||
|
autoUpdate,
|
||||||
|
offset,
|
||||||
|
flip,
|
||||||
|
shift,
|
||||||
|
useHover,
|
||||||
|
useFocus,
|
||||||
|
useDismiss,
|
||||||
|
useRole,
|
||||||
|
useInteractions,
|
||||||
|
FloatingPortal,
|
||||||
|
} from "@floating-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
content: string;
|
||||||
|
children: React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tooltip({ content, children }: TooltipProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
placement: "top",
|
||||||
|
// Keep in sync with scroll and resize
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
middleware: [\
|
||||||
|
offset(8), // Distance from reference\
|
||||||
|
flip(), // Flip to bottom if no space above\
|
||||||
|
shift({ padding: 8 }), // Shift horizontally to stay in viewport\
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Interaction hooks — compose behaviors
|
||||||
|
const hover = useHover(context, { move: false });
|
||||||
|
const focus = useFocus(context);
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
const role = useRole(context, { role: "tooltip" });
|
||||||
|
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([\
|
||||||
|
hover,\
|
||||||
|
focus,\
|
||||||
|
dismiss,\
|
||||||
|
role,\
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Attach to trigger element */}
|
||||||
|
{React.cloneElement(children, {
|
||||||
|
ref: refs.setReference,
|
||||||
|
...getReferenceProps(),
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Tooltip — rendered in portal to escape stacking contexts */}
|
||||||
|
<FloatingPortal>
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
ref={refs.setFloating}
|
||||||
|
style={{
|
||||||
|
...floatingStyles,
|
||||||
|
background: "#1a1a1a",
|
||||||
|
color: "#fff",
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
{...getFloatingProps()}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FloatingPortal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Tooltip content="Copy to clipboard">
|
||||||
|
<button>Copy</button>
|
||||||
|
</Tooltip>
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Arrow Placement](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#arrow-placement)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
useFloating,
|
||||||
|
arrow,
|
||||||
|
offset,
|
||||||
|
flip,
|
||||||
|
FloatingArrow,
|
||||||
|
} from "@floating-ui/react";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
export function TooltipWithArrow({ content, children }: TooltipProps) {
|
||||||
|
const arrowRef = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context, middlewareData, placement } = useFloating({
|
||||||
|
middleware: [\
|
||||||
|
offset(10),\
|
||||||
|
flip(),\
|
||||||
|
arrow({ element: arrowRef }),\
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={refs.setReference}>{children}</div>
|
||||||
|
|
||||||
|
<div ref={refs.setFloating} style={floatingStyles}>
|
||||||
|
{content}
|
||||||
|
{/* FloatingArrow renders an SVG arrow positioned correctly */}
|
||||||
|
<FloatingArrow
|
||||||
|
ref={arrowRef}
|
||||||
|
context={context}
|
||||||
|
fill="#1a1a1a"
|
||||||
|
height={8}
|
||||||
|
width={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Popover (Click-to-Open)](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#popover-click-to-open)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
useFloating,
|
||||||
|
autoUpdate,
|
||||||
|
offset,
|
||||||
|
flip,
|
||||||
|
shift,
|
||||||
|
useClick,
|
||||||
|
useDismiss,
|
||||||
|
useRole,
|
||||||
|
useInteractions,
|
||||||
|
FloatingPortal,
|
||||||
|
FloatingFocusManager,
|
||||||
|
} from "@floating-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function Popover({ trigger, content }: { trigger: React.ReactNode; content: React.ReactNode }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
placement: "bottom-start",
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const click = useClick(context);
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
const role = useRole(context, { role: "dialog" });
|
||||||
|
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={refs.setReference} {...getReferenceProps()}>
|
||||||
|
{trigger}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FloatingPortal>
|
||||||
|
{isOpen && (
|
||||||
|
// FloatingFocusManager traps focus inside the popover
|
||||||
|
<FloatingFocusManager context={context} modal={false}>
|
||||||
|
<div
|
||||||
|
ref={refs.setFloating}
|
||||||
|
style={{
|
||||||
|
...floatingStyles,
|
||||||
|
background: "#fff",
|
||||||
|
border: "1px solid #e2e8f0",
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: "0 4px 20px rgba(0,0,0,0.15)",
|
||||||
|
padding: 16,
|
||||||
|
zIndex: 9999,
|
||||||
|
minWidth: 200,
|
||||||
|
}}
|
||||||
|
{...getFloatingProps()}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</FloatingFocusManager>
|
||||||
|
)}
|
||||||
|
</FloatingPortal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Virtual Element (Context Menu)](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#virtual-element-context-menu)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useFloating, offset, flip, shift, useClientPoint, useInteractions } from "@floating-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function ContextMenu({ items }: { items: string[] }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
placement: "bottom-start",
|
||||||
|
middleware: [offset({ mainAxis: 5, alignmentAxis: 4 }), flip(), shift()],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Follow the mouse cursor
|
||||||
|
const clientPoint = useClientPoint(context);
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([clientPoint]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={refs.setReference}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
style={{ minHeight: 200, border: "1px dashed #ccc", padding: 16 }}
|
||||||
|
{...getReferenceProps()}
|
||||||
|
>
|
||||||
|
Right-click anywhere here
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
ref={refs.setFloating}
|
||||||
|
style={{
|
||||||
|
...floatingStyles,
|
||||||
|
background: "#fff",
|
||||||
|
border: "1px solid #e2e8f0",
|
||||||
|
borderRadius: 6,
|
||||||
|
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
{...getFloatingProps()}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
style={{ display: "block", width: "100%", padding: "8px 16px", textAlign: "left" }}
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Tippy.js: Batteries-Included Tooltips](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#tippyjs-batteries-included-tooltips)
|
||||||
|
|
||||||
|
Tippy.js provides a complete tooltip and popover solution with themes, animations, and a declarative API — minimal configuration required.
|
||||||
|
|
||||||
|
### [Installation](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#installation-1)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install tippy.js @tippyjs/react
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Basic Usage](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#basic-usage)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Tippy from "@tippyjs/react";
|
||||||
|
import "tippy.js/dist/tippy.css"; // Default theme
|
||||||
|
|
||||||
|
export function CopyButton() {
|
||||||
|
return (
|
||||||
|
<Tippy content="Copy to clipboard">
|
||||||
|
<button onClick={() => navigator.clipboard.writeText("text")}>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</Tippy>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Placement and Options](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#placement-and-options)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Tippy from "@tippyjs/react";
|
||||||
|
|
||||||
|
export function FeatureTooltips() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Tippy content="Shows above" placement="top">
|
||||||
|
<button>Top</button>
|
||||||
|
</Tippy>
|
||||||
|
|
||||||
|
<Tippy content="Shows on the right" placement="right">
|
||||||
|
<button>Right</button>
|
||||||
|
</Tippy>
|
||||||
|
|
||||||
|
{/* Delay: 300ms show, 100ms hide */}
|
||||||
|
<Tippy content="Delayed tooltip" delay={[300, 100]}>
|
||||||
|
<button>Delayed</button>
|
||||||
|
</Tippy>
|
||||||
|
|
||||||
|
{/* Click to toggle instead of hover */}
|
||||||
|
<Tippy content="Click me" trigger="click" interactive>
|
||||||
|
<button>Click</button>
|
||||||
|
</Tippy>
|
||||||
|
|
||||||
|
{/* Interactive (won't close when hovering tooltip) */}
|
||||||
|
<Tippy
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
<strong>Rich content</strong>
|
||||||
|
<p>With multiple elements</p>
|
||||||
|
<a href="/docs">Read more</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
interactive
|
||||||
|
interactiveBorder={20}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<button>Hover for rich tooltip</button>
|
||||||
|
</Tippy>
|
||||||
|
|
||||||
|
{/* Disabled */}
|
||||||
|
<Tippy content="Tooltip" disabled={false}>
|
||||||
|
<span>
|
||||||
|
<button disabled>Disabled Button</button>
|
||||||
|
</span>
|
||||||
|
</Tippy>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Animations and Themes](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#animations-and-themes)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Tippy from "@tippyjs/react";
|
||||||
|
import "tippy.js/dist/tippy.css";
|
||||||
|
import "tippy.js/animations/scale.css";
|
||||||
|
import "tippy.js/themes/light.css";
|
||||||
|
import "tippy.js/themes/material.css";
|
||||||
|
|
||||||
|
export function ThemedTooltips() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Built-in light theme */}
|
||||||
|
<Tippy content="Light theme" theme="light">
|
||||||
|
<button>Light</button>
|
||||||
|
</Tippy>
|
||||||
|
|
||||||
|
{/* Scale animation */}
|
||||||
|
<Tippy content="Animated" animation="scale">
|
||||||
|
<button>Scale</button>
|
||||||
|
</Tippy>
|
||||||
|
|
||||||
|
{/* Custom theme via CSS */}
|
||||||
|
<Tippy content="Custom theme" className="custom-tippy">
|
||||||
|
<button>Custom</button>
|
||||||
|
</Tippy>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Controlled Tippy](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#controlled-tippy)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Tippy from "@tippyjs/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function ControlledTooltip() {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tippy
|
||||||
|
content="This is controlled"
|
||||||
|
visible={visible}
|
||||||
|
onClickOutside={() => setVisible(false)}
|
||||||
|
interactive
|
||||||
|
>
|
||||||
|
<button onClick={() => setVisible((v) => !v)}>
|
||||||
|
{visible ? "Hide" : "Show"} Tooltip
|
||||||
|
</button>
|
||||||
|
</Tippy>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Radix UI Tooltip and Popover: Accessible Components](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#radix-ui-tooltip-and-popover-accessible-components)
|
||||||
|
|
||||||
|
Radix provides fully accessible, headless components with correct ARIA roles, focus management, and keyboard navigation — you bring your own styles.
|
||||||
|
|
||||||
|
### [Installation](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#installation-2)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @radix-ui/react-tooltip @radix-ui/react-popover
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Tooltip](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#tooltip)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
|
|
||||||
|
// Provider wraps your app — controls delay behavior globally
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<Tooltip.Provider delayDuration={300} skipDelayDuration={500}>
|
||||||
|
<YourApp />
|
||||||
|
</Tooltip.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual tooltip
|
||||||
|
export function DeleteButton() {
|
||||||
|
return (
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button className="icon-button" aria-label="Delete item">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content
|
||||||
|
className="tooltip-content"
|
||||||
|
sideOffset={4}
|
||||||
|
side="top"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
Delete item
|
||||||
|
<Tooltip.Arrow className="tooltip-arrow" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS
|
||||||
|
/*
|
||||||
|
.tooltip-content {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
animation-duration: 150ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
.tooltip-content[data-state='delayed-open'][data-side='top'] {
|
||||||
|
animation-name: slideDownAndFade;
|
||||||
|
}
|
||||||
|
@keyframes slideDownAndFade {
|
||||||
|
from { opacity: 0; transform: translateY(2px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.tooltip-arrow {
|
||||||
|
fill: #1a1a1a;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Popover](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#popover)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
|
export function FilterPopover() {
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<button className="filter-button">Filters ⚙️</button>
|
||||||
|
</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Portal>
|
||||||
|
<Popover.Content
|
||||||
|
className="popover-content"
|
||||||
|
sideOffset={4}
|
||||||
|
align="start"
|
||||||
|
// Prevent closing when focus moves inside popover
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<div className="filter-form">
|
||||||
|
<h3>Filter Options</h3>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Status
|
||||||
|
<select>
|
||||||
|
<option>All</option>
|
||||||
|
<option>Active</option>
|
||||||
|
<option>Inactive</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Date Range
|
||||||
|
<input type="date" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="filter-actions">
|
||||||
|
<button>Reset</button>
|
||||||
|
<Popover.Close asChild>
|
||||||
|
<button>Apply</button>
|
||||||
|
</Popover.Close>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover.Arrow className="popover-arrow" />
|
||||||
|
<Popover.Close className="popover-close" aria-label="Close">
|
||||||
|
✕
|
||||||
|
</Popover.Close>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Tooltip with Tailwind (shadcn/ui Pattern)](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#tooltip-with-tailwind-shadcnui-pattern)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// components/ui/tooltip.tsx — shadcn/ui Tooltip component
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider;
|
||||||
|
const TooltipRoot = TooltipPrimitive.Root;
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95",
|
||||||
|
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
));
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
// Export the Tooltip component
|
||||||
|
export function Tooltip({
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
content: React.ReactNode;
|
||||||
|
} & React.ComponentPropsWithoutRef<typeof TooltipRoot>) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<TooltipRoot {...props}>
|
||||||
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
|
<TooltipContent>{content}</TooltipContent>
|
||||||
|
</TooltipRoot>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage with Tailwind
|
||||||
|
<Tooltip content="Settings">
|
||||||
|
<button>⚙️</button>
|
||||||
|
</Tooltip>
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Feature Comparison](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#feature-comparison)
|
||||||
|
|
||||||
|
| Feature | Floating UI | Tippy.js | Radix Tooltip/Popover |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Framework** | Any (React, Vue, Svelte) | Any + React | React only |
|
||||||
|
| **Styling** | Unstyled (bring your own) | Styled (override available) | Unstyled (bring your own) |
|
||||||
|
| **Accessibility** | Manual (you implement) | Basic | ✅ WAI-ARIA compliant |
|
||||||
|
| **Focus trap** | `FloatingFocusManager` | No | ✅ Built-in |
|
||||||
|
| **Keyboard nav** | Via hooks | Basic | ✅ Built-in |
|
||||||
|
| **Collision detection** | ✅ Advanced | ✅ Via Popper.js | ✅ Via Floating UI |
|
||||||
|
| **Arrow positioning** | ✅ `FloatingArrow` | ✅ Built-in | ✅ `Tooltip.Arrow` |
|
||||||
|
| **Animations** | CSS (you define) | ✅ Built-in themes | CSS data-state |
|
||||||
|
| **Portal** | ✅ `FloatingPortal` | ✅ Auto | ✅ `Portal` |
|
||||||
|
| **Virtual elements** | ✅ | Limited | No |
|
||||||
|
| **Bundle size** | ~10kB | ~15kB | ~8kB per primitive |
|
||||||
|
| **npm weekly** | 12M | 3M | 8M (tooltip) |
|
||||||
|
| **GitHub stars** | 29k | 11k | 22k (radix-ui/primitives) |
|
||||||
|
| **TypeScript** | ✅ Full | ✅ | ✅ Full |
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [When to Use Each](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#when-to-use-each)
|
||||||
|
|
||||||
|
**Choose Floating UI if:**
|
||||||
|
|
||||||
|
- Building a component library from scratch (unstyled primitives)
|
||||||
|
- Need maximum control over positioning behavior and styling
|
||||||
|
- Framework-agnostic — Vue, Svelte, vanilla JS, or React
|
||||||
|
- Virtual element positioning (context menus, cursors)
|
||||||
|
- Complex middleware requirements (custom offset logic)
|
||||||
|
- Want to understand exactly what's happening — no magic
|
||||||
|
|
||||||
|
**Choose Tippy.js if:**
|
||||||
|
|
||||||
|
- Quick tooltip needed with minimal setup
|
||||||
|
- Vanilla JS project or legacy codebase
|
||||||
|
- Want built-in themes and animations without CSS work
|
||||||
|
- Simple hover tooltips where accessibility is secondary
|
||||||
|
- Prototyping or internal tools where ARIA isn't critical
|
||||||
|
|
||||||
|
**Choose Radix Tooltip/Popover if:**
|
||||||
|
|
||||||
|
- React/Next.js production application
|
||||||
|
- Accessibility is required — screen readers, keyboard navigation
|
||||||
|
- Using shadcn/ui (Radix is the foundation)
|
||||||
|
- Want compound component API with proper focus management
|
||||||
|
- Need `asChild` pattern to avoid extra DOM elements
|
||||||
|
- Building a design system where consumers control all styling
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Methodology](https://www.pkgpulse.com/blog/floating-ui-vs-tippyjs-vs-radix-tooltip-popover-2026\#methodology)
|
||||||
|
|
||||||
|
Data sourced from Floating UI documentation (floating-ui.com/docs), Tippy.js documentation (atomiks.github.io/tippyjs), Radix UI documentation (radix-ui.com/docs), npm weekly download statistics as of February 2026, GitHub star counts as of February 2026, and community discussions from the React Discord, CSS-Tricks, and web accessibility forums.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
_Related: [Radix UI vs Headless UI vs Ariakit](https://www.pkgpulse.com/blog/radix-ui-vs-headless-ui-vs-ariakit-accessible-react-components-2026) for broader headless component comparisons, or [shadcn/ui vs Mantine vs Chakra UI](https://www.pkgpulse.com/blog/shadcn-vs-mantine-vs-chakra-react-component-library-2026) for styled React component libraries._
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
### The 2026 JavaScript Stack Cheatsheet
|
||||||
|
|
||||||
|
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
|
||||||
|
|
||||||
|
Get the Free Cheatsheet
|
||||||
305
.firecrawl/pkgpulse-linting.md
Normal file
305
.firecrawl/pkgpulse-linting.md
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
[Skip to main content](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026#main-content)
|
||||||
|
|
||||||
|
## [TL;DR](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026\#tldr)
|
||||||
|
|
||||||
|
**Biome is production-ready for most JavaScript and TypeScript projects, but ESLint + Prettier is still the right call if you need the full ESLint plugin ecosystem.** Biome's 25x speed advantage is real and meaningful in CI. The formatter is nearly identical to Prettier. The linter covers ~80% of common ESLint rules. What Biome can't replace yet: type-aware lint rules (requires TypeScript language service), framework-specific plugins (eslint-plugin-react-hooks, eslint-plugin-next), and any custom ESLint rules your team has written. Verdict: new projects → Biome. Existing projects with heavy plugin usage → evaluate the gap before switching.
|
||||||
|
|
||||||
|
## [Key Takeaways](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026\#key-takeaways)
|
||||||
|
|
||||||
|
- **Speed**: Biome formats and lints in ~50ms; ESLint + Prettier takes ~2-3s for same project
|
||||||
|
- **Coverage**: ~250 lint rules (growing); ESLint has 1000+ with the plugin ecosystem
|
||||||
|
- **Prettier compatibility**: Biome's formatter matches Prettier output for ~96% of cases
|
||||||
|
- **Not yet in Biome**: type-aware rules, React Hooks rules, Next.js plugin, custom rule authoring (in roadmap)
|
||||||
|
- **Configuration**: one config file (`biome.json`) vs two separate configs
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Speed: The Main Reason to Switch](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026\#speed-the-main-reason-to-switch)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Real benchmark — a Next.js project with ~150 TypeScript files:
|
||||||
|
|
||||||
|
# ESLint + Prettier (separate runs):
|
||||||
|
npx eslint . --ext .ts,.tsx → 3.2 seconds
|
||||||
|
npx prettier --check "**/*.{ts,tsx}" → 1.1 seconds
|
||||||
|
Total: ~4.3 seconds
|
||||||
|
|
||||||
|
# Biome (lint + format together):
|
||||||
|
npx biome check . → 0.18 seconds
|
||||||
|
# 24x faster combined
|
||||||
|
|
||||||
|
# CI impact (running on every PR):
|
||||||
|
# ESLint + Prettier: 4-5 seconds of your CI job
|
||||||
|
# Biome: ~0.2 seconds
|
||||||
|
|
||||||
|
# Pre-commit hooks (runs on staged files):
|
||||||
|
# ESLint + Prettier (lint-staged): ~1.5s per commit
|
||||||
|
# Biome: ~0.05s per commit
|
||||||
|
# Developer experience: the difference between "imperceptible" and "I notice this every time"
|
||||||
|
|
||||||
|
# Why Biome is faster:
|
||||||
|
# → Rust implementation (not Node.js)
|
||||||
|
# → Parallel processing of files
|
||||||
|
# → Single pass: lint + format in one traversal
|
||||||
|
# → No plugin loading overhead (rules are compiled in)
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Setup: One Config vs Two Configs](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026\#setup-one-config-vs-two-configs)
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Biome — biome.json (one file for everything):
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"correctness": {
|
||||||
|
"noUnusedVariables": "error"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"noParameterAssign": "warn"
|
||||||
|
},
|
||||||
|
"nursery": {
|
||||||
|
"useSortedClasses": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineWidth": 100
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "double",
|
||||||
|
"trailingCommas": "all",
|
||||||
|
"semicolons": "always"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignore": ["node_modules", "dist", ".next"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare to ESLint + Prettier:
|
||||||
|
// .eslintrc.json OR eslint.config.mjs (ESLint 9 flat config)
|
||||||
|
// .prettierrc
|
||||||
|
// .prettierignore
|
||||||
|
// .eslintignore
|
||||||
|
// package.json scripts to run both
|
||||||
|
// lint-staged config for pre-commit hooks
|
||||||
|
|
||||||
|
// Biome replaces all of that with one file.
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Lint Rules Coverage](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026\#lint-rules-coverage)
|
||||||
|
|
||||||
|
```
|
||||||
|
Biome rules (v1.9, 2026) — grouped by ESLint equivalent:
|
||||||
|
|
||||||
|
✅ Covered well:
|
||||||
|
→ no-unused-vars, no-undef → biome: correctness/noUnusedVariables
|
||||||
|
→ no-console → biome: suspicious/noConsole
|
||||||
|
→ eqeqeq → biome: suspicious/noDoubleEquals
|
||||||
|
→ no-var → biome: style/noVar
|
||||||
|
→ prefer-const → biome: style/useConst
|
||||||
|
→ no-empty → biome: correctness/noEmptyBlockStatements
|
||||||
|
→ no-duplicate-imports → biome: correctness/noDuplicateObjectKeys
|
||||||
|
→ arrow-body-style → biome: style/useArrowFunction
|
||||||
|
→ object-shorthand → biome: style/useShorthandAssign
|
||||||
|
→ 200+ more rules...
|
||||||
|
|
||||||
|
⚠️ Partially covered / different API:
|
||||||
|
→ import/order → biome: organizeImports (reorders, doesn't configure)
|
||||||
|
→ jsx-a11y/* → basic accessibility rules, not all of jsx-a11y
|
||||||
|
|
||||||
|
❌ Not yet in Biome:
|
||||||
|
→ Type-aware rules (requires TypeScript type checker)
|
||||||
|
→ @typescript-eslint/no-floating-promises
|
||||||
|
→ @typescript-eslint/no-misused-promises
|
||||||
|
→ @typescript-eslint/consistent-return (typed)
|
||||||
|
→ eslint-plugin-react-hooks (useEffect deps, hooks rules)
|
||||||
|
→ eslint-plugin-next (app router patterns, image optimization rules)
|
||||||
|
→ eslint-plugin-import/no-cycle (circular dependency detection)
|
||||||
|
→ Custom rules your team wrote
|
||||||
|
|
||||||
|
The gap is real but smaller than it was.
|
||||||
|
Most "critical" lint rules are covered.
|
||||||
|
The missing ones are important for React/Next.js specifically.
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Prettier Compatibility: How Close Is It?](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026\#prettier-compatibility-how-close-is-it)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Biome's formatter is designed to match Prettier's output
|
||||||
|
// Real-world compatibility: ~96% identical for JS/TS
|
||||||
|
|
||||||
|
// Cases where they differ (edge cases):
|
||||||
|
// 1. Long template literals
|
||||||
|
const query = `SELECT * FROM users WHERE id = ${userId} AND status = 'active' AND created_at > '2024-01-01'`;
|
||||||
|
// Prettier: keeps on one line if fits, wraps differently
|
||||||
|
// Biome: similar but not identical on complex expressions
|
||||||
|
|
||||||
|
// 2. Decorators (TypeScript)
|
||||||
|
// Some class decorator formatting differs slightly
|
||||||
|
|
||||||
|
// 3. Complex JSX expressions
|
||||||
|
// Multi-line JSX attributes format slightly differently in edge cases
|
||||||
|
|
||||||
|
// For the vast majority of code: identical output.
|
||||||
|
// The 4% difference is in edge cases you'll rarely hit.
|
||||||
|
|
||||||
|
// Migration from Prettier:
|
||||||
|
# Run Biome formatter on your entire codebase once:
|
||||||
|
npx biome format --write .
|
||||||
|
|
||||||
|
# Check what changed (should be minimal):
|
||||||
|
git diff
|
||||||
|
|
||||||
|
# If there are many meaningful diffs, the code was inconsistently formatted.
|
||||||
|
# Biome will now be the source of truth.
|
||||||
|
|
||||||
|
# Teams commonly report:
|
||||||
|
# → 0-20 files changed on a typical codebase
|
||||||
|
# → Changes are whitespace/trailing comma in edge cases
|
||||||
|
# → No semantic code changes
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Integration with Editors and CI](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026\#integration-with-editors-and-ci)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# VS Code — install the Biome extension:
|
||||||
|
# Extensions: "Biome" (biomejs.biome)
|
||||||
|
# settings.json:
|
||||||
|
{
|
||||||
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"[javascript]": { "editor.defaultFormatter": "biomejs.biome" },
|
||||||
|
"[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
|
||||||
|
"[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }
|
||||||
|
}
|
||||||
|
# The extension works well — fast, accurate
|
||||||
|
|
||||||
|
# Pre-commit hooks (replace lint-staged + eslint/prettier):
|
||||||
|
# package.json:
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "simple-git-hooks"
|
||||||
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"pre-commit": "npx lint-staged"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,ts,jsx,tsx,json,css}": "biome check --apply --no-errors-on-unmatched"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# CI (GitHub Actions):
|
||||||
|
- name: Lint and Format Check
|
||||||
|
run: npx biome ci .
|
||||||
|
# biome ci = check without --write; exits non-zero if issues found
|
||||||
|
# Replaces separate eslint and prettier --check steps
|
||||||
|
|
||||||
|
# The biome ci command is designed exactly for this use case.
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Migration from ESLint + Prettier](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026\#migration-from-eslint--prettier)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Initialize Biome
|
||||||
|
npm install --save-dev --save-exact @biomejs/biome
|
||||||
|
|
||||||
|
# Step 2: Generate config from existing ESLint config
|
||||||
|
npx biome migrate eslint --include-inspired
|
||||||
|
# --include-inspired: adds Biome rules "inspired by" your ESLint rules
|
||||||
|
|
||||||
|
# Step 3: Format with Biome once (commit this separately for clean history)
|
||||||
|
npx biome format --write .
|
||||||
|
git add -A && git commit -m "chore: migrate formatter to Biome"
|
||||||
|
|
||||||
|
# Step 4: Fix linting issues
|
||||||
|
npx biome check --apply .
|
||||||
|
# Some will be auto-fixed. Others need manual attention.
|
||||||
|
|
||||||
|
# Step 5: Find the ESLint rules you'll miss
|
||||||
|
# Go through your .eslintrc and categorize:
|
||||||
|
# → Rule covered by Biome? → Remove from ESLint
|
||||||
|
# → Rule is react-hooks or type-aware? → Keep ESLint for JUST those rules
|
||||||
|
|
||||||
|
# Step 6: The hybrid approach (if you need react-hooks rules):
|
||||||
|
# Keep ESLint for only what Biome doesn't cover:
|
||||||
|
# .eslintrc:
|
||||||
|
{
|
||||||
|
"plugins": ["react-hooks"],
|
||||||
|
"rules": {
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "warn"
|
||||||
|
// Nothing else — Biome handles the rest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# This is the recommended migration path for React projects.
|
||||||
|
# Use Biome for formatting + most linting.
|
||||||
|
# Keep a minimal ESLint config for react-hooks only.
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Verdict: Should You Switch?](https://www.pkgpulse.com/blog/biome-vs-eslint-prettier-linting-2026\#verdict-should-you-switch)
|
||||||
|
|
||||||
|
```
|
||||||
|
New greenfield project (2026):
|
||||||
|
→ Yes — use Biome from day one
|
||||||
|
→ Add minimal ESLint config for react-hooks if it's a React project
|
||||||
|
→ The speed win in CI is real; the single config is a DX improvement
|
||||||
|
→ The rule gap doesn't matter if you're starting fresh
|
||||||
|
|
||||||
|
Existing project (small, no complex ESLint plugins):
|
||||||
|
→ Yes — migrate. 2-4 hour job. Net positive.
|
||||||
|
→ Use the migrate command; review diffs; ship it
|
||||||
|
|
||||||
|
Existing project (React/Next.js, heavy plugin usage):
|
||||||
|
→ Hybrid approach — Biome for format + most lint, ESLint for react-hooks + next
|
||||||
|
→ Not "switch" but "add Biome alongside a reduced ESLint config"
|
||||||
|
→ You still get the speed benefit for most of the work
|
||||||
|
|
||||||
|
Existing project (custom ESLint rules, type-aware rules critical):
|
||||||
|
→ Not yet — monitor Biome's type-aware rule roadmap
|
||||||
|
→ Expected in late 2026 based on their public roadmap
|
||||||
|
→ Reevaluate in 6 months
|
||||||
|
|
||||||
|
The trajectory is clear: Biome is getting better fast.
|
||||||
|
The rule gap that seemed large in 2024 is substantially smaller in 2026.
|
||||||
|
Type-aware rules are the final frontier.
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
_Compare Biome, ESLint, and Prettier download trends at [PkgPulse](https://www.pkgpulse.com/)._
|
||||||
|
|
||||||
|
See the live comparison
|
||||||
|
|
||||||
|
[View biome vs. eslint on PkgPulse →](https://www.pkgpulse.com/compare/biome-vs-eslint)
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
### The 2026 JavaScript Stack Cheatsheet
|
||||||
|
|
||||||
|
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
|
||||||
|
|
||||||
|
Get the Free Cheatsheet
|
||||||
256
.firecrawl/pkgpulse-packagemgr.md
Normal file
256
.firecrawl/pkgpulse-packagemgr.md
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
[Skip to main content](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026#main-content)
|
||||||
|
|
||||||
|
## [TL;DR](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#tldr)
|
||||||
|
|
||||||
|
**pnpm is the 2026 default for serious JavaScript projects — content-addressable store, strict dependency isolation, and the best monorepo support.** Bun is 5-10x faster than pnpm on installs but still has edge cases with niche packages. npm is the default that works everywhere but is the slowest. For new projects: pnpm (or Bun if you're already in the Bun ecosystem). For CI speed: Bun's install is often faster than even pnpm's cached install.
|
||||||
|
|
||||||
|
## [Key Takeaways](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#key-takeaways)
|
||||||
|
|
||||||
|
- **Bun install**: 5-10x faster than pnpm, 15-25x faster than npm (measured on real projects)
|
||||||
|
- **pnpm**: Strictest isolation (prevents phantom dependencies), best workspace support, most compatible
|
||||||
|
- **npm**: Default, slowest, but universally compatible, `node_modules` phantom deps allowed
|
||||||
|
- **Disk usage**: pnpm uses ~50% less disk space vs npm (content-addressable store deduplication)
|
||||||
|
- **Monorepos**: pnpm workspaces > Bun workspaces > npm workspaces (feature parity gap)
|
||||||
|
- **2026 recommendation**: pnpm for serious projects; Bun install if on Bun runtime already
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Downloads / Usage](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#downloads--usage)
|
||||||
|
|
||||||
|
| Package Manager | Weekly Downloads | Trend |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `npm` | Default (Node.js) | → Stable |
|
||||||
|
| `pnpm` | ~7M downloads/week | ↑ Growing |
|
||||||
|
| `bun` | ~1.5M downloads/week | ↑ Fast growing |
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Install Speed Benchmarks](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#install-speed-benchmarks)
|
||||||
|
|
||||||
|
```
|
||||||
|
Benchmark: Next.js 15 project (1,847 packages)
|
||||||
|
Environment: M3 MacBook Pro, SSD, cold/warm cache
|
||||||
|
|
||||||
|
COLD INSTALL (no cache, no lockfile):
|
||||||
|
npm: 82s
|
||||||
|
pnpm: 31s (2.6x faster than npm)
|
||||||
|
Bun: 8s (10x faster than npm)
|
||||||
|
|
||||||
|
CACHED INSTALL (lockfile present, store exists):
|
||||||
|
npm: 45s (reads node_modules hash)
|
||||||
|
pnpm: 4s (hardlinks from content store)
|
||||||
|
Bun: 0.8s (binary cache, near-instant)
|
||||||
|
|
||||||
|
CI INSTALL (lockfile present, fresh machine):
|
||||||
|
npm: 62s
|
||||||
|
pnpm: 18s (3.4x faster)
|
||||||
|
Bun: 6s (10x faster)
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [pnpm: The Recommended Default](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#pnpm-the-recommended-default)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install pnpm:
|
||||||
|
npm install -g pnpm
|
||||||
|
# Or via Corepack (Node.js built-in):
|
||||||
|
corepack enable pnpm
|
||||||
|
|
||||||
|
# Common commands:
|
||||||
|
pnpm install # Install from lockfile
|
||||||
|
pnpm add react # Add dependency
|
||||||
|
pnpm add -D typescript # Add dev dependency
|
||||||
|
pnpm remove lodash # Remove package
|
||||||
|
pnpm update --interactive # Interactive update UI
|
||||||
|
pnpm why lodash # Why is this installed?
|
||||||
|
pnpm ls # List installed packages
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .npmrc — pnpm configuration:
|
||||||
|
# Enforce strict peer dependencies:
|
||||||
|
strict-peer-dependencies=true
|
||||||
|
|
||||||
|
# Hoist patterns (allow certain phantom deps for compat):
|
||||||
|
public-hoist-pattern[]=*eslint*
|
||||||
|
public-hoist-pattern[]=*prettier*
|
||||||
|
|
||||||
|
# Save exact versions:
|
||||||
|
save-exact=true
|
||||||
|
|
||||||
|
# Node linker (for compatibility with some tools):
|
||||||
|
# node-linker=hoisted # Falls back to npm-style if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
// pnpm-workspace.yaml — monorepo config:
|
||||||
|
{
|
||||||
|
"packages": [\
|
||||||
|
"apps/*",\
|
||||||
|
"packages/*",\
|
||||||
|
"tools/*"\
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# pnpm workspace commands:
|
||||||
|
pnpm --filter web add react-query # Add to specific package
|
||||||
|
pnpm --filter "!web" install # Install all except web
|
||||||
|
pnpm -r run build # Run build in all packages
|
||||||
|
pnpm --filter web... run build # Build web + its dependencies
|
||||||
|
pnpm --filter ...web run build # Build packages that depend on web
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Why pnpm Over npm](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#why-pnpm-over-npm)
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm advantages:
|
||||||
|
→ No phantom dependencies (package.json must declare everything)
|
||||||
|
→ 50% less disk usage (hardlinks, not copies)
|
||||||
|
→ 3-5x faster installs than npm
|
||||||
|
→ Best workspace support (filtering, recursive)
|
||||||
|
→ Isolated node_modules (each package sees only its deps)
|
||||||
|
|
||||||
|
pnpm limitations:
|
||||||
|
→ Occasional compatibility issues with poorly-written packages
|
||||||
|
→ Slightly steeper learning curve for teams migrating from npm
|
||||||
|
→ Some tools (older ones) expect hoisted node_modules
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Bun: When Speed Is Everything](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#bun-when-speed-is-everything)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Bun:
|
||||||
|
curl -fsSL https://bun.sh/install | bash
|
||||||
|
|
||||||
|
# Bun install commands (compatible with npm syntax):
|
||||||
|
bun install # Install from lockfile
|
||||||
|
bun add react # Add dependency
|
||||||
|
bun add -d typescript # Add dev dependency (note: -d not -D)
|
||||||
|
bun remove lodash # Remove
|
||||||
|
bun update # Update all packages
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# bun.lock — Bun's lockfile format:
|
||||||
|
# Binary lockfile (bun.lockb) in older versions
|
||||||
|
# Text lockfile (bun.lock) in Bun 1.1+
|
||||||
|
# Commit bun.lock to version control
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# bunfig.toml — Bun configuration:
|
||||||
|
[install]
|
||||||
|
# Use a private registry:
|
||||||
|
registry = "https://registry.npmjs.org"
|
||||||
|
exact = true # Pin exact versions
|
||||||
|
|
||||||
|
[install.scopes]
|
||||||
|
# Scoped registry:
|
||||||
|
"@mycompany" = { token = "$NPM_TOKEN", url = "https://npm.mycompany.com" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bun workspaces:
|
||||||
|
# package.json at root:
|
||||||
|
# {
|
||||||
|
# "workspaces": ["apps/*", "packages/*"]
|
||||||
|
# }
|
||||||
|
|
||||||
|
bun install # Installs all workspaces
|
||||||
|
bun add react --workspace apps/web # Add to specific workspace
|
||||||
|
bun run --filter '*' build # Run build in all workspaces
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Bun Install Limitations](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#bun-install-limitations)
|
||||||
|
|
||||||
|
```
|
||||||
|
Known compatibility issues in 2026:
|
||||||
|
→ Some native binaries may not install correctly
|
||||||
|
→ Postinstall scripts: some packages assume npm/node environment
|
||||||
|
→ pnpm-specific workspace.yaml not supported (use package.json workspaces)
|
||||||
|
→ Some packages with complex resolution logic may resolve differently
|
||||||
|
|
||||||
|
Test your project before switching to Bun install in CI:
|
||||||
|
bun install && bun test # Quick compatibility check
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [npm: Universal Compatibility](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#npm-universal-compatibility)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm — the universal fallback:
|
||||||
|
npm install # Install
|
||||||
|
npm install react # Add
|
||||||
|
npm install -D typescript # Add dev
|
||||||
|
npm uninstall lodash # Remove
|
||||||
|
npm update # Update
|
||||||
|
|
||||||
|
# npm workspaces (basic):
|
||||||
|
# package.json: { "workspaces": ["apps/*", "packages/*"] }
|
||||||
|
npm install # Installs all workspaces
|
||||||
|
npm run build --workspace=apps/web # Run in specific workspace
|
||||||
|
npm run build --workspaces # Run in all workspaces
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Corepack: Managing Package Managers](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#corepack-managing-package-managers)
|
||||||
|
|
||||||
|
```json
|
||||||
|
// package.json — specify exact package manager:
|
||||||
|
{
|
||||||
|
"packageManager": "pnpm@9.15.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable Corepack (Node.js 16+):
|
||||||
|
corepack enable
|
||||||
|
|
||||||
|
# Now the packageManager field is enforced:
|
||||||
|
# If you run npm install in a pnpm project, Corepack intercepts:
|
||||||
|
# "This project requires pnpm@9.15.0. Run 'corepack use pnpm@9.15.0' to switch."
|
||||||
|
|
||||||
|
# In CI — enable Corepack before install:
|
||||||
|
corepack enable
|
||||||
|
# Then just run: pnpm install (or whatever packageManager specifies)
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Decision Guide](https://www.pkgpulse.com/blog/pnpm-vs-bun-vs-npm-package-manager-performance-2026\#decision-guide)
|
||||||
|
|
||||||
|
```
|
||||||
|
Use pnpm if:
|
||||||
|
→ New project, want best practices
|
||||||
|
→ Monorepo with multiple packages
|
||||||
|
→ Strict dependency isolation important
|
||||||
|
→ Most compatible choice that's still fast
|
||||||
|
|
||||||
|
Use Bun (install) if:
|
||||||
|
→ Already using Bun as runtime
|
||||||
|
→ CI speed is critical and you've tested compatibility
|
||||||
|
→ Greenfield project with modern packages only
|
||||||
|
|
||||||
|
Use npm if:
|
||||||
|
→ Maximum compatibility needed (legacy projects)
|
||||||
|
→ Required by tooling that expects npm conventions
|
||||||
|
→ Team unfamiliar with pnpm/Bun
|
||||||
|
→ Deploying to environment where only npm is available
|
||||||
|
```
|
||||||
|
|
||||||
|
_Compare package manager downloads on [PkgPulse](https://pkgpulse.com/)._
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
### The 2026 JavaScript Stack Cheatsheet
|
||||||
|
|
||||||
|
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
|
||||||
|
|
||||||
|
Get the Free Cheatsheet
|
||||||
377
.firecrawl/pkgpulse-reactaria-radix.md
Normal file
377
.firecrawl/pkgpulse-reactaria-radix.md
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
[Skip to main content](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026#main-content)
|
||||||
|
|
||||||
|
Accessibility litigation against web applications increased by 30% year-over-year in 2025. Building accessible UI has gone from "nice to have" to legal requirement for many organizations — and the right headless component library can be the difference between compliance and a lawsuit. React Aria (Adobe) and Radix Primitives are the two dominant choices, and they differ profoundly in philosophy.
|
||||||
|
|
||||||
|
## [TL;DR](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#tldr)
|
||||||
|
|
||||||
|
**Radix Primitives** is the right default for most React applications — pragmatic, well-documented, widely adopted (60K+ stars), and accessible enough for the vast majority of use cases. **React Aria** is the right choice when accessibility is a primary constraint, WCAG compliance is required by contract, or you need the strictest ARIA pattern implementation available. Both are significantly better than building accessible components from scratch.
|
||||||
|
|
||||||
|
## [Key Takeaways](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#key-takeaways)
|
||||||
|
|
||||||
|
- Radix Primitives: ~3.5M weekly downloads across packages, 60K+ GitHub stars
|
||||||
|
- React Aria Components: ~260K weekly downloads for `react-aria-components`
|
||||||
|
- Radix: 28 main components; React Aria: 43+ components
|
||||||
|
- React Aria is built by Adobe (accessibility specialists), used in Adobe Spectrum
|
||||||
|
- Radix is the foundation of Shadcn UI — the most popular component library of 2025
|
||||||
|
- React Aria strictly follows WCAG 2.1 patterns; Radix prioritizes pragmatic DX with good accessibility
|
||||||
|
- Both ship behavior/logic only — you provide the styles (headless architecture)
|
||||||
|
|
||||||
|
## [The Headless Component Model](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#the-headless-component-model)
|
||||||
|
|
||||||
|
Both libraries are "headless" — they provide behavior, accessibility, and keyboard interactions, but **no visual styling**. You bring your own CSS (Tailwind, CSS Modules, etc.).
|
||||||
|
|
||||||
|
This is the right architecture for component libraries: it separates behavior concerns from visual concerns, making the library usable in any design system.
|
||||||
|
|
||||||
|
## [Radix Primitives](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#radix-primitives)
|
||||||
|
|
||||||
|
**Packages**: `@radix-ui/react-*` (28+ packages)
|
||||||
|
**Downloads**: ~3.5M weekly (across all packages)
|
||||||
|
**GitHub stars**: 16K (primitives repo), 60K+ (Radix UI org)
|
||||||
|
**Created by**: WorkOS
|
||||||
|
|
||||||
|
Radix is the most popular headless component library in the React ecosystem, largely because it powers Shadcn UI.
|
||||||
|
|
||||||
|
### [Installation](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#installation)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install individual primitives as needed
|
||||||
|
npm install @radix-ui/react-dialog
|
||||||
|
npm install @radix-ui/react-dropdown-menu
|
||||||
|
npm install @radix-ui/react-select
|
||||||
|
npm install @radix-ui/react-tooltip
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Usage Pattern](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#usage-pattern)
|
||||||
|
|
||||||
|
Radix uses a compound component pattern:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
|
||||||
|
function DeleteConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
|
||||||
|
return (
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<button className="btn-danger">Delete Account</button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 bg-black/50 animate-fade-in" />
|
||||||
|
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 shadow-xl">
|
||||||
|
<Dialog.Title className="text-lg font-semibold">
|
||||||
|
Confirm Deletion
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description className="text-gray-600 mt-2">
|
||||||
|
This action cannot be undone. All your data will be permanently deleted.
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<div className="flex gap-3 mt-6">
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button className="btn-secondary">Cancel</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button className="btn-danger" onClick={onConfirm}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [`asChild` Prop](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#aschild-prop)
|
||||||
|
|
||||||
|
One of Radix's best DX features: `asChild` lets you apply Radix behavior to any component without adding extra DOM elements:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
|
import { Link } from 'next/link'; // Or any custom component
|
||||||
|
|
||||||
|
// Without asChild: wraps in a <button>
|
||||||
|
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
||||||
|
|
||||||
|
// With asChild: applies trigger behavior to Link directly
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<Link href="/docs">Documentation</Link>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Data Attributes for Styling](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#data-attributes-for-styling)
|
||||||
|
|
||||||
|
Radix exposes state via `data-` attributes, making CSS targeting clean:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Style based on component state */
|
||||||
|
[data-state="open"] > .trigger-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-highlighted] {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-disabled] {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Available Components (2026)](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#available-components-2026)
|
||||||
|
|
||||||
|
Accordion, Alert Dialog, Aspect Ratio, Avatar, Checkbox, Collapsible, Context Menu, Dialog, Dropdown Menu, Form, Hover Card, Label, Menubar, Navigation Menu, Popover, Progress, Radio Group, Scroll Area, Select, Separator, Slider, Switch, Tabs, Toast, Toggle, Toggle Group, Toolbar, Tooltip.
|
||||||
|
|
||||||
|
## [React Aria](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#react-aria)
|
||||||
|
|
||||||
|
**Package**: `react-aria-components` (all-in-one), or individual `@react-aria/*` hooks
|
||||||
|
**Downloads**: ~260K weekly (`react-aria-components`)
|
||||||
|
**GitHub stars**: 13K (react-spectrum monorepo)
|
||||||
|
**Created by**: Adobe
|
||||||
|
|
||||||
|
React Aria comes from Adobe's design systems team — the same team that builds accessibility-focused products used by millions. It implements ARIA patterns from the WAI-ARIA Authoring Practices Guide more strictly than any other library.
|
||||||
|
|
||||||
|
### [Installation](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#installation-1)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All-in-one package (recommended for new projects)
|
||||||
|
npm install react-aria-components
|
||||||
|
|
||||||
|
# Or individual hooks for granular control
|
||||||
|
npm install @react-aria/dialog @react-aria/focus @react-stately/dialog
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Component API](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#component-api)
|
||||||
|
|
||||||
|
React Aria uses a render props pattern that gives you maximum control:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Dialog, DialogTrigger, Modal, ModalOverlay, Button, Heading } from 'react-aria-components';
|
||||||
|
|
||||||
|
function DeleteConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button className="btn-danger">Delete Account</Button>
|
||||||
|
|
||||||
|
<ModalOverlay className="fixed inset-0 bg-black/50 animate-fade-in">
|
||||||
|
<Modal className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 shadow-xl">
|
||||||
|
<Dialog>
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<Heading slot="title" className="text-lg font-semibold">
|
||||||
|
Confirm Deletion
|
||||||
|
</Heading>
|
||||||
|
<p className="text-gray-600 mt-2">
|
||||||
|
This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3 mt-6">
|
||||||
|
<Button onPress={close} className="btn-secondary">Cancel</Button>
|
||||||
|
<Button onPress={() => { onConfirm(); close(); }} className="btn-danger">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</ModalOverlay>
|
||||||
|
</DialogTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### [CSS Classes with State](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#css-classes-with-state)
|
||||||
|
|
||||||
|
React Aria uses a slightly different styling approach:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// renderProps pattern for dynamic classes
|
||||||
|
<Button
|
||||||
|
className={({ isPressed, isFocused, isDisabled }) =>
|
||||||
|
`btn ${isPressed ? 'btn-pressed' : ''} ${isFocused ? 'btn-focused' : ''}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Click me
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with data attributes (similar to Radix):
|
||||||
|
|
||||||
|
```css
|
||||||
|
.btn[data-pressed] { transform: scale(0.98); }
|
||||||
|
.btn[data-focused] { outline: 2px solid var(--color-focus); }
|
||||||
|
.btn[data-disabled] { opacity: 0.5; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Advanced Accessibility Features](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#advanced-accessibility-features)
|
||||||
|
|
||||||
|
React Aria handles edge cases that simpler libraries miss:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ComboBox with proper ARIA pattern
|
||||||
|
import { ComboBox, Item, Label, Input, Popover, ListBox } from 'react-aria-components';
|
||||||
|
|
||||||
|
function SearchComboBox() {
|
||||||
|
return (
|
||||||
|
<ComboBox>
|
||||||
|
<Label>Search countries</Label>
|
||||||
|
<Input />
|
||||||
|
<Popover>
|
||||||
|
<ListBox>
|
||||||
|
<Item>Afghanistan</Item>
|
||||||
|
<Item>Albania</Item>
|
||||||
|
{/* ... */}
|
||||||
|
</ListBox>
|
||||||
|
</Popover>
|
||||||
|
</ComboBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
React Aria correctly handles:
|
||||||
|
|
||||||
|
- Proper `combobox` ARIA role with `aria-expanded`, `aria-haspopup`, `aria-owns`
|
||||||
|
- `aria-activedescendant` updates on keyboard navigation
|
||||||
|
- Correct focus management when popup opens/closes
|
||||||
|
- Mobile: virtual cursor navigation for screen readers on iOS/Android
|
||||||
|
- Touch: proper pointer events for touch screen accessibility
|
||||||
|
|
||||||
|
### [Hook-Level API](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#hook-level-api)
|
||||||
|
|
||||||
|
For more control, you can use the individual hooks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useButton } from '@react-aria/button';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
function CustomButton({ onPress, children }: Props) {
|
||||||
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
|
const { buttonProps } = useButton({ onPress }, ref);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...buttonProps}
|
||||||
|
ref={ref}
|
||||||
|
className="custom-button"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## [Accessibility Comparison](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#accessibility-comparison)
|
||||||
|
|
||||||
|
### [ARIA Compliance](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#aria-compliance)
|
||||||
|
|
||||||
|
| Component | Radix | React Aria |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Dialog | WCAG AA | WCAG AAA |
|
||||||
|
| Select | WCAG AA | WCAG AAA |
|
||||||
|
| Combobox | Good | Strict |
|
||||||
|
| Date Picker | Not included | Full ARIA pattern |
|
||||||
|
| Grid | Not included | Full keyboard nav |
|
||||||
|
| Virtual cursor | Partial | Full iOS/Android |
|
||||||
|
|
||||||
|
### [Screen Reader Testing](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#screen-reader-testing)
|
||||||
|
|
||||||
|
React Aria tests against: JAWS + Chrome, NVDA + Firefox, VoiceOver + Safari, TalkBack + Chrome, VoiceOver + iOS Safari.
|
||||||
|
|
||||||
|
Radix tests against major screen readers but with less rigor on mobile platforms.
|
||||||
|
|
||||||
|
## [Components Available (React Aria vs Radix)](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#components-available-react-aria-vs-radix)
|
||||||
|
|
||||||
|
| Component | Radix | React Aria |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Button | No (basic) | Yes |
|
||||||
|
| Checkbox | Yes | Yes |
|
||||||
|
| Dialog/Modal | Yes | Yes |
|
||||||
|
| Dropdown Menu | Yes | Yes |
|
||||||
|
| Select | Yes | Yes |
|
||||||
|
| Combobox | No | Yes |
|
||||||
|
| Date Picker | No | Yes |
|
||||||
|
| Calendar | No | Yes |
|
||||||
|
| Color Picker | No | Yes |
|
||||||
|
| Table/Grid | No | Yes |
|
||||||
|
| Drag & Drop | No | Yes |
|
||||||
|
| File Trigger | No | Yes |
|
||||||
|
| Tag Group | No | Yes |
|
||||||
|
|
||||||
|
React Aria has significantly more components, especially for complex interactive patterns.
|
||||||
|
|
||||||
|
## [Bundle Size](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#bundle-size)
|
||||||
|
|
||||||
|
| Package | Gzipped |
|
||||||
|
| --- | --- |
|
||||||
|
| `@radix-ui/react-dialog` | ~5.5 kB |
|
||||||
|
| `@radix-ui/react-dropdown-menu` | ~12 kB |
|
||||||
|
| `react-aria-components` (tree-shaken) | ~8-20 kB per component |
|
||||||
|
|
||||||
|
## [The Shadcn Effect](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#the-shadcn-effect)
|
||||||
|
|
||||||
|
Radix's dominant download numbers are largely the Shadcn UI effect. Shadcn UI is built on Radix Primitives + Tailwind CSS, and it became the most-copied component library of 2025. Every team using Shadcn is installing Radix under the hood:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# This installs @radix-ui/react-dialog automatically
|
||||||
|
npx shadcn-ui add dialog
|
||||||
|
```
|
||||||
|
|
||||||
|
There's no Shadcn-equivalent built on React Aria (though Argos CI migrated from Radix to React Aria and published a migration guide).
|
||||||
|
|
||||||
|
## [When to Choose Each](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#when-to-choose-each)
|
||||||
|
|
||||||
|
**Choose Radix Primitives if:**
|
||||||
|
|
||||||
|
- You're using Shadcn UI (Radix is already included)
|
||||||
|
- You want the largest community and most resources
|
||||||
|
- "Accessible enough" is acceptable (not strict WCAG AAA)
|
||||||
|
- Time-to-production is important
|
||||||
|
- Your design system is already partially built
|
||||||
|
|
||||||
|
**Choose React Aria if:**
|
||||||
|
|
||||||
|
- WCAG AA/AAA compliance is contractually required
|
||||||
|
- You need Date Picker, Color Picker, Drag & Drop, or Table with full keyboard navigation
|
||||||
|
- Your product is used by enterprise customers with accessibility requirements
|
||||||
|
- You build products used by people with disabilities (government, healthcare, education)
|
||||||
|
- Mobile screen reader support is critical
|
||||||
|
|
||||||
|
## [A Pragmatic Hybrid](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#a-pragmatic-hybrid)
|
||||||
|
|
||||||
|
Some teams use Radix for most components and React Aria specifically for the complex ones Radix doesn't handle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu # Most UI
|
||||||
|
npm install react-aria-components # Date pickers, complex patterns
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a valid approach — they're both headless libraries that don't conflict.
|
||||||
|
|
||||||
|
## [Compare on PkgPulse](https://www.pkgpulse.com/blog/react-aria-vs-radix-primitives-2026\#compare-on-pkgpulse)
|
||||||
|
|
||||||
|
Track download trends for [Radix UI vs React Aria on PkgPulse](https://pkgpulse.com/).
|
||||||
|
|
||||||
|
See the live comparison
|
||||||
|
|
||||||
|
[View react aria vs. radix on PkgPulse →](https://www.pkgpulse.com/compare/react-aria-vs-radix)
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
giscus
|
||||||
|
|
||||||
|
#### 0 reactions
|
||||||
|
|
||||||
|
#### 0 comments
|
||||||
|
|
||||||
|
WritePreview
|
||||||
|
|
||||||
|
[Styling with Markdown is supported](https://guides.github.com/features/mastering-markdown/ "Styling with Markdown is supported")
|
||||||
|
|
||||||
|
[Sign in with GitHub](https://giscus.app/api/oauth/authorize?redirect_uri=https%3A%2F%2Fwww.pkgpulse.com%2Fblog%2Freact-aria-vs-radix-primitives-2026)
|
||||||
|
|
||||||
|
### The 2026 JavaScript Stack Cheatsheet
|
||||||
|
|
||||||
|
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
|
||||||
|
|
||||||
|
Get the Free Cheatsheet
|
||||||
372
.firecrawl/pkgpulse-shadcn-baseui-radix.md
Normal file
372
.firecrawl/pkgpulse-shadcn-baseui-radix.md
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
[Skip to main content](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026#main-content)
|
||||||
|
|
||||||
|
The React component ecosystem in 2026 looks nothing like it did three years ago. shadcn/ui went from "interesting experiment" to the default choice for new React projects, collecting 75,000+ GitHub stars in the process. Radix UI — the primitive layer that shadcn originally built on top of — has slowed down since WorkOS acquired it. And Base UI emerged from the MUI team as a serious contender that addresses Radix's architectural shortcomings with production backing from the world's most-downloaded React component library.
|
||||||
|
|
||||||
|
The three aren't directly competing. They represent different layers of the component stack, and understanding the relationship between them changes which one you should actually install.
|
||||||
|
|
||||||
|
## [TL;DR](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#tldr)
|
||||||
|
|
||||||
|
**For most new React projects: shadcn/ui** — it's the industry default for good reason, the February 2026 Visual Builder reduces setup friction to near zero, and it now supports both Radix and Base UI as the underlying primitive layer. **For custom design systems that need unstyled primitives: Base UI** — it's better-maintained than Radix and has cleaner APIs for complex interactions (comboboxes, multi-select, nested menus). **For existing Radix-based projects: keep Radix** unless you have specific pain points — migration isn't worth the disruption for things that work.
|
||||||
|
|
||||||
|
## [Key Takeaways](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#key-takeaways)
|
||||||
|
|
||||||
|
- **shadcn/ui hit 75,000+ stars** — GitHub's most-starred React UI project for the third consecutive year
|
||||||
|
- **shadcn/ui now supports Base UI primitives** in addition to Radix — you can choose your primitive layer
|
||||||
|
- **The February 2026 Visual Builder** lets you configure components visually and copies the exact code — no more manual Tailwind class customization
|
||||||
|
- **Radix UI was acquired by WorkOS** — updates have slowed, particularly for complex components like Combobox and multi-select
|
||||||
|
- **Base UI is MUI-backed** with dedicated full-time engineering, not a side project
|
||||||
|
- **Base UI has better TypeScript types** and cleaner APIs for complex interaction patterns
|
||||||
|
- **130M monthly npm downloads for Radix** — it's not going anywhere, but active development is slower
|
||||||
|
- **"Headless UI" is the wrong mental model for shadcn/ui** — it ships with styles, but you own them
|
||||||
|
|
||||||
|
## [At a Glance](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#at-a-glance)
|
||||||
|
|
||||||
|
| | shadcn/ui | Base UI | Radix UI |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| GitHub stars | 75,000+ | 4,200+ | 18,700+ |
|
||||||
|
| npm downloads/month | ~20M | ~5M | ~130M |
|
||||||
|
| Styling approach | Tailwind (you own the code) | Unstyled primitives | Unstyled primitives |
|
||||||
|
| Install model | Copy-paste into your repo | npm package | npm package |
|
||||||
|
| Backing | Community / Vercel interest | MUI team (full-time) | WorkOS (acquired) |
|
||||||
|
| TypeScript | ✅ | ✅ Excellent | ✅ Good |
|
||||||
|
| Accessibility | ✅ (via primitives) | ✅ | ✅ |
|
||||||
|
| Combobox / multi-select | ✅ (via cmdk) | ✅ Better API | ⚠️ Limited |
|
||||||
|
| Animation primitives | ✅ | ✅ | ✅ |
|
||||||
|
| CSS Variables theming | ✅ | ✅ | ✅ |
|
||||||
|
| Visual Builder | ✅ (Feb 2026) | ❌ | ❌ |
|
||||||
|
| Bundle size (dialog) | ~8KB | ~6KB | ~9KB |
|
||||||
|
| React version | 18/19 | 18/19 | 18 (19 in progress) |
|
||||||
|
|
||||||
|
## [What Each Actually Is](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#what-each-actually-is)
|
||||||
|
|
||||||
|
Understanding these three tools requires being clear about what layer they occupy:
|
||||||
|
|
||||||
|
**Radix UI** and **Base UI** are _headless component primitives_ — they handle accessibility, keyboard navigation, ARIA attributes, and interaction logic with zero styling. You get behavior, not appearance. You add the CSS.
|
||||||
|
|
||||||
|
**shadcn/ui** is _not a component library in the npm sense_. It's a collection of pre-built components using Tailwind CSS, built on top of headless primitives (originally Radix, now Base UI too). When you run `npx shadcn@latest add button`, it copies the source code for a Button component into your project at `components/ui/button.tsx`. You then own that code — you can modify it however you want. There's no package to update.
|
||||||
|
|
||||||
|
This distinction matters for how you evaluate them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Radix/Base UI — installed as a dependency, updated via npm
|
||||||
|
npm install @radix-ui/react-dialog
|
||||||
|
npm install @base-ui-components/react
|
||||||
|
|
||||||
|
# shadcn/ui — components copied into your project
|
||||||
|
npx shadcn@latest add dialog
|
||||||
|
# Creates: components/ui/dialog.tsx
|
||||||
|
# You own this file. Edit it freely.
|
||||||
|
```
|
||||||
|
|
||||||
|
## [shadcn/ui: The Copy-Paste Revolution](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#shadcnui-the-copy-paste-revolution)
|
||||||
|
|
||||||
|
The "install npm package vs copy into your project" distinction sounds like a minor implementation detail. In practice, it changes the entire maintenance model:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// After running: npx shadcn@latest add dialog
|
||||||
|
// This file is in YOUR repo at components/ui/dialog.tsx
|
||||||
|
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
You own this code. You see every className. You can change the animation, the overlay color, the z-index, the transition timing — all without fighting a library's theming system. Want a drawer instead of a centered modal? Edit the classes.
|
||||||
|
|
||||||
|
This is fundamentally different from installing MUI or Mantine, where customizing deeply means fighting overrides.
|
||||||
|
|
||||||
|
## [The February 2026 Visual Builder](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#the-february-2026-visual-builder)
|
||||||
|
|
||||||
|
shadcn's biggest 2026 addition is the Visual Builder — an interactive configurator that lets you adjust variants, sizes, and appearances visually, then copies the exact Tailwind code for your specific configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# No install needed — open the builder at:
|
||||||
|
# https://ui.shadcn.com/builder
|
||||||
|
|
||||||
|
# It generates the exact component code for your choices:
|
||||||
|
# - Variant (default/destructive/outline/secondary/ghost/link)
|
||||||
|
# - Size (default/sm/lg/icon)
|
||||||
|
# - Custom colors/spacing
|
||||||
|
|
||||||
|
# Then copy the output to your components/ui/button.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
For teams that struggled with "what Tailwind classes produce the exact style I want," this removes the main friction point. You configure visually, get the code, own the result.
|
||||||
|
|
||||||
|
## [Switching shadcn/ui from Radix to Base UI](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#switching-shadcnui-from-radix-to-base-ui)
|
||||||
|
|
||||||
|
The February 2026 update that went somewhat under the radar: shadcn/ui now officially supports Base UI as the underlying primitive layer, in addition to Radix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize shadcn with Base UI primitives (new in 2026)
|
||||||
|
npx shadcn@latest init --base-ui
|
||||||
|
|
||||||
|
# Or add individual components using Base UI
|
||||||
|
npx shadcn@latest add dialog --primitive=base-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
The resulting components look identical to the user, but the underlying primitive has better TypeScript types and a more consistent API. For new projects, this is worth choosing. For existing Radix-based shadcn projects, migration isn't required.
|
||||||
|
|
||||||
|
## [Base UI: MUI's Headless Bet](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#base-ui-muis-headless-bet)
|
||||||
|
|
||||||
|
Base UI is the MUI (Material UI) team's headless component library, extracted from their earlier "Unstyled components" offering and rebuilt from the ground up. The key difference from Radix:
|
||||||
|
|
||||||
|
**Full-time engineering backing.** Radix is maintained by the WorkOS team with a small core team. Base UI has dedicated MUI engineers working on it as a primary product investment. MUI serves millions of developers — they have strong incentives to keep Base UI production-quality.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Base UI Dialog — cleaner API surface
|
||||||
|
import * as Dialog from "@base-ui-components/react/dialog";
|
||||||
|
|
||||||
|
function ConfirmDialog({ open, onOpenChange, onConfirm }) {
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Backdrop className="dialog-backdrop" />
|
||||||
|
<Dialog.Popup className="dialog-popup">
|
||||||
|
<Dialog.Title>Confirm Action</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
This action cannot be undone.
|
||||||
|
</Dialog.Description>
|
||||||
|
<div className="dialog-actions">
|
||||||
|
<Dialog.Close>Cancel</Dialog.Close>
|
||||||
|
<button onClick={onConfirm}>Confirm</button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Popup>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The naming is cleaner: `Dialog.Popup` instead of `DialogContent`, `Dialog.Backdrop` instead of `DialogOverlay`. Minor difference in isolation, but consistent across all components.
|
||||||
|
|
||||||
|
### [Base UI's Combobox: Where It Genuinely Wins](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#base-uis-combobox-where-it-genuinely-wins)
|
||||||
|
|
||||||
|
Radix has been criticized for weak support for complex interaction patterns, particularly comboboxes and multi-select. The Radix Select primitive is intentionally limited, and building a proper searchable multi-select on top of it requires significant custom code:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Radix approach to searchable select — requires cmdk workaround
|
||||||
|
import * as Select from "@radix-ui/react-select";
|
||||||
|
// Radix Select doesn't support search natively
|
||||||
|
// shadcn/ui uses cmdk (Command) for this pattern
|
||||||
|
|
||||||
|
// Base UI Combobox — built-in search support
|
||||||
|
import * as Combobox from "@base-ui-components/react/combobox";
|
||||||
|
|
||||||
|
function TagSelector({ tags, onSelect }) {
|
||||||
|
return (
|
||||||
|
<Combobox.Root multiple>
|
||||||
|
<Combobox.Input placeholder="Search tags..." />
|
||||||
|
<Combobox.Popup>
|
||||||
|
{tags.map(tag => (
|
||||||
|
<Combobox.Item key={tag.id} value={tag.id}>
|
||||||
|
<Combobox.ItemText>{tag.name}</Combobox.ItemText>
|
||||||
|
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
|
||||||
|
</Combobox.Item>
|
||||||
|
))}
|
||||||
|
</Combobox.Popup>
|
||||||
|
</Combobox.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Multi-select combobox with search, built natively
|
||||||
|
```
|
||||||
|
|
||||||
|
If your application needs complex form interactions — multi-select dropdowns, searchable selects, tag inputs — Base UI's primitive set handles them more naturally than Radix.
|
||||||
|
|
||||||
|
## [Radix UI: The Market Leader with a Question Mark](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#radix-ui-the-market-leader-with-a-question-mark)
|
||||||
|
|
||||||
|
With 130M monthly downloads, Radix UI is the most widely used headless component library in the React ecosystem. It's the primitive layer under shadcn/ui (in its default configuration), Mantine UI, and dozens of other component libraries.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Radix — the primitives you're probably already using
|
||||||
|
import * as Dialog from "@radix-ui/react-dialog";
|
||||||
|
import * as Select from "@radix-ui/react-select";
|
||||||
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
|
// Fully accessible, keyboard navigable, WAI-ARIA compliant
|
||||||
|
// Every property is documented, every pattern tested
|
||||||
|
```
|
||||||
|
|
||||||
|
The concern isn't whether Radix is good — it is good. The concern is trajectory. After WorkOS acquired Radix, the update cadence slowed. Several long-standing issues (combobox support, React 19 compatibility updates) moved slowly through the pipeline. The core team is smaller than Base UI's now.
|
||||||
|
|
||||||
|
For projects already built on Radix: don't migrate. The primitives work, the ecosystem is massive, and the API won't break overnight. For new projects choosing between Radix and Base UI as a primitive foundation: Base UI has momentum on its side.
|
||||||
|
|
||||||
|
### [React 19 Compatibility](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#react-19-compatibility)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Radix — React 19 compatibility update was delayed
|
||||||
|
// Some @radix-ui/* packages needed updates for React 19's new ref handling
|
||||||
|
// Most updated by late 2025, but caused upgrade friction
|
||||||
|
|
||||||
|
// Base UI — built with React 19 in mind from the start
|
||||||
|
// No compatibility issues with the new ref transformation
|
||||||
|
```
|
||||||
|
|
||||||
|
Teams upgrading to React 19 encountered some rough edges with Radix packages that took months to fully resolve. Base UI was designed after React 19's changes were known and avoids these patterns.
|
||||||
|
|
||||||
|
## [Bundle Size Reality](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#bundle-size-reality)
|
||||||
|
|
||||||
|
```
|
||||||
|
Component: Dialog (open/close with animation)
|
||||||
|
|
||||||
|
Radix @radix-ui/react-dialog:
|
||||||
|
Package size: 9.2KB (gzipped)
|
||||||
|
Dependencies: @radix-ui/react-portal, @radix-ui/primitive, etc.
|
||||||
|
Total with deps: ~28KB
|
||||||
|
|
||||||
|
Base UI @base-ui-components/react (dialog only):
|
||||||
|
Package size: 6.4KB (gzipped)
|
||||||
|
Self-contained: yes (fewer cross-package deps)
|
||||||
|
Total: ~18KB
|
||||||
|
|
||||||
|
shadcn/ui Dialog (your compiled code + Radix):
|
||||||
|
Component code: ~3KB (Tailwind, compiled)
|
||||||
|
Runtime: Radix primitives (see above)
|
||||||
|
Total: ~31KB but includes styles
|
||||||
|
```
|
||||||
|
|
||||||
|
The numbers are close enough that bundle size shouldn't drive your decision. What matters more: Radix has many cross-package dependencies (each primitive is a separate package), which can inflate your `node_modules` significantly on larger projects. Base UI is more self-contained.
|
||||||
|
|
||||||
|
## [TypeScript Integration](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#typescript-integration)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Radix — solid TypeScript, component-level types
|
||||||
|
import * as Dialog from "@radix-ui/react-dialog";
|
||||||
|
|
||||||
|
// The types are there but sometimes overly broad
|
||||||
|
type DialogContentProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof Dialog.Content
|
||||||
|
>; // Works but verbose
|
||||||
|
|
||||||
|
// Base UI — excellent TypeScript, consistent naming
|
||||||
|
import * as Dialog from "@base-ui-components/react/dialog";
|
||||||
|
|
||||||
|
// Sub-path imports give you tree-shaking + precise types
|
||||||
|
type DialogPopupProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof Dialog.Popup
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Base UI components consistently expose:
|
||||||
|
// - render prop for custom element types
|
||||||
|
// - className string or function receiving state
|
||||||
|
// - All HTML attributes via ...props
|
||||||
|
function StyledDialog({ open }: { open: boolean }) {
|
||||||
|
return (
|
||||||
|
<Dialog.Popup
|
||||||
|
// className can be a function receiving component state
|
||||||
|
className={(state) =>
|
||||||
|
state.open ? "dialog-open" : "dialog-closed"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Base UI's state-based className function is a pattern that makes conditional styling clean without needing `data-[state=open]:` Tailwind selectors.
|
||||||
|
|
||||||
|
## [Choosing Your Stack in 2026](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#choosing-your-stack-in-2026)
|
||||||
|
|
||||||
|
**Recommendation for new Next.js/React projects:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install shadcn/ui with Base UI primitives (best of both worlds)
|
||||||
|
npx shadcn@latest init --base-ui
|
||||||
|
|
||||||
|
# 2. Add components as you need them
|
||||||
|
npx shadcn@latest add button dialog dropdown-menu tooltip
|
||||||
|
|
||||||
|
# 3. Customize the generated code in components/ui/
|
||||||
|
# — You own it. Tailwind classes are yours to change.
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives you: shadcn's pre-built components and Visual Builder, Base UI's better primitives under the hood, and ownership of all the code.
|
||||||
|
|
||||||
|
**For custom design systems (no shadcn):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Base UI — unstyled primitives with modern API
|
||||||
|
npm install @base-ui-components/react
|
||||||
|
|
||||||
|
# Use CSS modules, CSS-in-JS, or Tailwind — your choice
|
||||||
|
# Better comboboxes, cleaner types, active development
|
||||||
|
```
|
||||||
|
|
||||||
|
**For existing Radix-based projects:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Don't migrate unless you have specific pain points.
|
||||||
|
# Radix works. 130M downloads/month says so.
|
||||||
|
# Wait for shadcn/ui's migration tooling if you ever want to switch.
|
||||||
|
```
|
||||||
|
|
||||||
|
## [The Three-Layer Mental Model](https://www.pkgpulse.com/blog/shadcn-ui-vs-base-ui-vs-radix-components-2026\#the-three-layer-mental-model)
|
||||||
|
|
||||||
|
The cleanest mental model for understanding these three:
|
||||||
|
|
||||||
|
```
|
||||||
|
Layer 3: shadcn/ui
|
||||||
|
↓ Pre-built, Tailwind-styled, copy-pasted into your project
|
||||||
|
↓ Uses Layer 2 as primitives (configurable: Radix or Base UI)
|
||||||
|
|
||||||
|
Layer 2: Radix UI / Base UI
|
||||||
|
↓ Unstyled, accessible, behavior-only
|
||||||
|
↓ You provide all styling
|
||||||
|
↓ Both implement ARIA patterns correctly
|
||||||
|
|
||||||
|
Layer 1: Your application
|
||||||
|
↓ Uses whichever combination serves your needs
|
||||||
|
```
|
||||||
|
|
||||||
|
Most developers don't need to choose between all three. They choose shadcn/ui (which bundles the decision about Layer 2) or they choose a headless primitive directly (Radix or Base UI) for a custom design system. The 2026 update that shadcn/ui supports both Radix and Base UI as primitive layers means you're no longer locked in.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
_Compare shadcn/ui, Base UI, and Radix UI download trends at [PkgPulse](https://www.pkgpulse.com/compare/shadcn-ui-vs-radix-ui)._
|
||||||
|
|
||||||
|
_Related: [React 19 vs React 18 2026](https://www.pkgpulse.com/blog/react-19-vs-react-18-2026) · [Tailwind CSS vs CSS Modules 2026](https://www.pkgpulse.com/blog/tailwind-vs-css-modules-2026) · [MUI vs Ant Design 2026](https://www.pkgpulse.com/blog/mui-vs-ant-design-2026)_
|
||||||
|
|
||||||
|
See the live comparison
|
||||||
|
|
||||||
|
[View shadcn ui vs. radix ui on PkgPulse →](https://www.pkgpulse.com/compare/shadcn-ui-vs-radix-ui)
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
giscus
|
||||||
|
|
||||||
|
#### 0 reactions
|
||||||
|
|
||||||
|
#### 0 comments
|
||||||
|
|
||||||
|
WritePreview
|
||||||
|
|
||||||
|
[Styling with Markdown is supported](https://guides.github.com/features/mastering-markdown/ "Styling with Markdown is supported")
|
||||||
|
|
||||||
|
[Sign in with GitHub](https://giscus.app/api/oauth/authorize?redirect_uri=https%3A%2F%2Fwww.pkgpulse.com%2Fblog%2Fshadcn-ui-vs-base-ui-vs-radix-components-2026)
|
||||||
|
|
||||||
|
### The 2026 JavaScript Stack Cheatsheet
|
||||||
|
|
||||||
|
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
|
||||||
|
|
||||||
|
Get the Free Cheatsheet
|
||||||
302
.firecrawl/pkgpulse-testing.md
Normal file
302
.firecrawl/pkgpulse-testing.md
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
[Skip to main content](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026#main-content)
|
||||||
|
|
||||||
|
## [TL;DR](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#tldr)
|
||||||
|
|
||||||
|
**The modern testing stack: Vitest (unit/integration) + Playwright (E2E).** Jest is still running millions of tests across the npm ecosystem, but new projects default to Vitest because it shares Vite's config, runs tests in parallel by default, and is 5-10x faster. Playwright replaced Cypress as the E2E tool of choice — better multi-tab support, less flakiness, and first-class TypeScript. The old stack (Jest + Enzyme/React Testing Library + Cypress) still works, but the new stack (Vitest + Testing Library + Playwright) is faster, simpler, and better.
|
||||||
|
|
||||||
|
## [Key Takeaways](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#key-takeaways)
|
||||||
|
|
||||||
|
- **Vitest**: Jest-compatible API, Vite-native, ~10x faster, TypeScript without setup
|
||||||
|
- **Jest**: 40M+ weekly downloads (legacy), still excellent, no reason to migrate working tests
|
||||||
|
- **Playwright**: multi-browser E2E, trace viewer, 80%+ market share in new projects
|
||||||
|
- **Cypress**: real-time browser view is great DX but slower and less capable than Playwright
|
||||||
|
- **Testing Library**: the default React component testing approach — framework-agnostic
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Unit Testing: Vitest vs Jest](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#unit-testing-vitest-vs-jest)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// The APIs are nearly identical — migration is usually find-and-replace:
|
||||||
|
|
||||||
|
// ─── Jest ───
|
||||||
|
// jest.config.js
|
||||||
|
module.exports = {
|
||||||
|
transform: { '^.+\\.tsx?$': ['ts-jest', {}] }, // setup required
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
};
|
||||||
|
|
||||||
|
// test file:
|
||||||
|
import { sum } from './math';
|
||||||
|
describe('math utils', () => {
|
||||||
|
test('adds two numbers', () => {
|
||||||
|
expect(sum(1, 2)).toBe(3);
|
||||||
|
});
|
||||||
|
it('handles negatives', () => {
|
||||||
|
expect(sum(-1, -2)).toBe(-3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Vitest ───
|
||||||
|
// vite.config.ts (reuses existing Vite config!)
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true, // optional: makes describe/test/expect global without import
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// test file — identical to Jest:
|
||||||
|
import { sum } from './math';
|
||||||
|
describe('math utils', () => {
|
||||||
|
test('adds two numbers', () => {
|
||||||
|
expect(sum(1, 2)).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Performance comparison (500 unit tests, React project):
|
||||||
|
// Jest (with ts-jest): 8.4s
|
||||||
|
// Jest (with babel-jest): 11.2s
|
||||||
|
// Vitest: 1.8s 🏆
|
||||||
|
|
||||||
|
// Why Vitest is faster:
|
||||||
|
// → Uses esbuild for transforms (same as Vite dev server)
|
||||||
|
// → Parallel by default (worker threads, one per test file)
|
||||||
|
// → No separate config for TS — shares Vite's esbuild config
|
||||||
|
// → Module resolution uses Vite's resolver (no duplicate setup)
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Component Testing with React Testing Library](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#component-testing-with-react-testing-library)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Testing Library works with both Jest and Vitest — same API:
|
||||||
|
|
||||||
|
// Setup (Vitest):
|
||||||
|
// package.json:
|
||||||
|
{
|
||||||
|
"test": "vitest",
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/react": "^15",
|
||||||
|
"@testing-library/user-event": "^14",
|
||||||
|
"@testing-library/jest-dom": "^6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// vitest.setup.ts:
|
||||||
|
import '@testing-library/jest-dom/vitest'; // extends expect with toBeInDocument etc.
|
||||||
|
|
||||||
|
// vite.config.ts:
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./vitest.setup.ts'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Component test:
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { LoginForm } from './LoginForm';
|
||||||
|
|
||||||
|
describe('LoginForm', () => {
|
||||||
|
it('submits with email and password', async () => {
|
||||||
|
const mockSubmit = vi.fn(); // vi.fn() instead of jest.fn()
|
||||||
|
render(<LoginForm onSubmit={mockSubmit} />);
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByLabelText('Email'), 'user@example.com');
|
||||||
|
await userEvent.type(screen.getByLabelText('Password'), 'password123');
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockSubmit).toHaveBeenCalledWith({
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error for invalid email', async () => {
|
||||||
|
render(<LoginForm onSubmit={vi.fn()} />);
|
||||||
|
await userEvent.type(screen.getByLabelText('Email'), 'not-an-email');
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
|
||||||
|
expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [E2E Testing: Playwright vs Cypress](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#e2e-testing-playwright-vs-cypress)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ─── Playwright ───
|
||||||
|
// playwright.config.ts
|
||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
trace: 'on-first-retry', // Capture traces on failure
|
||||||
|
},
|
||||||
|
projects: [\
|
||||||
|
{ name: 'chromium', use: { browserName: 'chromium' } },\
|
||||||
|
{ name: 'firefox', use: { browserName: 'firefox' } },\
|
||||||
|
{ name: 'safari', use: { browserName: 'webkit' } },\
|
||||||
|
{ name: 'mobile', use: { ...devices['iPhone 13'] } },\
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// e2e/auth.spec.ts
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('user can log in', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.fill('[data-testid="email"]', 'user@example.com');
|
||||||
|
await page.fill('[data-testid="password"]', 'password123');
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
await expect(page).toHaveURL('/dashboard');
|
||||||
|
await expect(page.locator('h1')).toHaveText('Welcome back');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Multi-tab test (Playwright exclusive):
|
||||||
|
test('cart persists across tabs', async ({ context }) => {
|
||||||
|
const page1 = await context.newPage();
|
||||||
|
const page2 = await context.newPage();
|
||||||
|
await page1.goto('/product/1');
|
||||||
|
await page1.click('button:text("Add to Cart")');
|
||||||
|
await page2.goto('/cart');
|
||||||
|
await expect(page2.locator('.cart-item')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// API mocking in tests:
|
||||||
|
test('shows error when API fails', async ({ page }) => {
|
||||||
|
await page.route('**/api/users', route => route.fulfill({
|
||||||
|
status: 500,
|
||||||
|
body: JSON.stringify({ error: 'Server error' }),
|
||||||
|
}));
|
||||||
|
await page.goto('/users');
|
||||||
|
await expect(page.locator('.error-message')).toBeVisible();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Playwright Trace Viewer: Debugging E2E Failures](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#playwright-trace-viewer-debugging-e2e-failures)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests with trace on failure:
|
||||||
|
npx playwright test --trace on
|
||||||
|
|
||||||
|
# Or configure in playwright.config.ts:
|
||||||
|
use: { trace: 'on-first-retry' }
|
||||||
|
|
||||||
|
# After a failure, view the trace:
|
||||||
|
npx playwright show-trace test-results/trace.zip
|
||||||
|
|
||||||
|
# The trace viewer shows:
|
||||||
|
# → Screenshot at each action
|
||||||
|
# → Network requests and responses
|
||||||
|
# → Console logs and errors
|
||||||
|
# → DOM snapshots you can inspect
|
||||||
|
# → Timeline of the test execution
|
||||||
|
# This replaces hours of debugging with 5 minutes of trace review
|
||||||
|
|
||||||
|
# Run specific test in headed mode (see the browser):
|
||||||
|
npx playwright test --headed auth.spec.ts
|
||||||
|
|
||||||
|
# Generate test code by recording browser actions:
|
||||||
|
npx playwright codegen http://localhost:3000
|
||||||
|
# → Opens browser, records your clicks, generates test code
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Complete Testing Stack Setup](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#complete-testing-stack-setup)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install everything for the modern stack:
|
||||||
|
npm install --save-dev \
|
||||||
|
vitest \
|
||||||
|
@vitest/ui \ # visual test runner UI
|
||||||
|
jsdom \ # browser environment for unit tests
|
||||||
|
@testing-library/react \
|
||||||
|
@testing-library/user-event \
|
||||||
|
@testing-library/jest-dom \
|
||||||
|
@playwright/test
|
||||||
|
|
||||||
|
# Install Playwright browsers (one-time):
|
||||||
|
npx playwright install
|
||||||
|
|
||||||
|
# package.json scripts:
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:watch": "vitest --watch",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:all": "vitest run && playwright test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# File structure:
|
||||||
|
src/
|
||||||
|
components/
|
||||||
|
Button/
|
||||||
|
Button.tsx
|
||||||
|
Button.test.tsx ← unit/integration test (Vitest + Testing Library)
|
||||||
|
utils/
|
||||||
|
math.test.ts ← unit test
|
||||||
|
e2e/
|
||||||
|
auth.spec.ts ← E2E test (Playwright)
|
||||||
|
checkout.spec.ts
|
||||||
|
playwright.config.ts
|
||||||
|
vite.config.ts ← Vitest config lives here
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [When to Keep Jest](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#when-to-keep-jest)
|
||||||
|
|
||||||
|
```
|
||||||
|
Keep Jest when:
|
||||||
|
→ Existing test suite works — don't migrate for the sake of it
|
||||||
|
→ Your project doesn't use Vite (Create React App, custom Webpack setup)
|
||||||
|
→ You use Jest-specific features (jest.spyOn, jest.useFakeTimers) extensively
|
||||||
|
→ Your team knows Jest deeply and migration would cause disruption
|
||||||
|
|
||||||
|
Migrate to Vitest when:
|
||||||
|
→ New project (always use Vitest)
|
||||||
|
→ Test suite is slow and painful (10+ second runs for unit tests)
|
||||||
|
→ You've already migrated to Vite for bundling
|
||||||
|
→ TypeScript setup with ts-jest is causing friction
|
||||||
|
|
||||||
|
Migration process (from Jest to Vitest):
|
||||||
|
1. npx vitest-migration # automated codemods available
|
||||||
|
2. Replace jest.fn() → vi.fn()
|
||||||
|
3. Replace jest.mock() → vi.mock()
|
||||||
|
4. Update jest.config.js → vitest config in vite.config.ts
|
||||||
|
5. Run tests: expect ~95% to pass without changes
|
||||||
|
|
||||||
|
The compatibility is excellent — most Jest tests run on Vitest unchanged.
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
_Compare Vitest, Jest, Playwright, and other testing library trends at [PkgPulse](https://www.pkgpulse.com/compare/vitest-vs-jest)._
|
||||||
|
|
||||||
|
See the live comparison
|
||||||
|
|
||||||
|
[View vitest vs. jest on PkgPulse →](https://www.pkgpulse.com/compare/vitest-vs-jest)
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
### The 2026 JavaScript Stack Cheatsheet
|
||||||
|
|
||||||
|
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
|
||||||
|
|
||||||
|
Get the Free Cheatsheet
|
||||||
290
.firecrawl/pkgpulse-validation.md
Normal file
290
.firecrawl/pkgpulse-validation.md
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
[Skip to main content](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026#main-content)
|
||||||
|
|
||||||
|
## [TL;DR](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026\#tldr)
|
||||||
|
|
||||||
|
**Zod v4 remains the default for TypeScript validation — but Valibot (8KB vs Zod's 60KB) and ArkType (fastest runtime parsing) are compelling for performance-critical use cases.** TypeBox generates JSON Schema natively, making it the best choice for OpenAPI/Swagger integration. For new projects: Zod v4. For edge/bundle-size-critical: Valibot. For OpenAPI: TypeBox. For maximum runtime speed: ArkType.
|
||||||
|
|
||||||
|
## [Key Takeaways](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026\#key-takeaways)
|
||||||
|
|
||||||
|
- **Zod v4**: 60KB, 10M+ downloads/week, best ecosystem (react-hook-form, trpc, drizzle)
|
||||||
|
- **Valibot**: 8KB, tree-shakable, modular API, ~10x smaller than Zod
|
||||||
|
- **ArkType**: Fastest parser (3-10x faster than Zod), TypeScript syntax strings
|
||||||
|
- **TypeBox**: JSON Schema native, `Static<typeof Schema>` TypeScript types
|
||||||
|
- **Performance**: ArkType > Valibot > TypeBox > Zod (but all are "fast enough" for most apps)
|
||||||
|
- **Ecosystem**: Zod integrates with everything; others are catching up
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Downloads](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026\#downloads)
|
||||||
|
|
||||||
|
| Package | Weekly Downloads | Trend |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `zod` | ~10M | ↑ Growing |
|
||||||
|
| `@sinclair/typebox` | ~6M | ↑ Growing |
|
||||||
|
| `valibot` | ~1M | ↑ Fast growing |
|
||||||
|
| `arktype` | ~200K | ↑ Growing |
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Performance Benchmarks](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026\#performance-benchmarks)
|
||||||
|
|
||||||
|
```
|
||||||
|
Schema: User object with 10 fields, nested address, array of tags
|
||||||
|
|
||||||
|
Parsing 100,000 objects:
|
||||||
|
ArkType: 45ms ← Fastest
|
||||||
|
Valibot: 120ms
|
||||||
|
TypeBox: 180ms
|
||||||
|
Zod v4: 280ms (v4 is 2x faster than v3's ~580ms)
|
||||||
|
|
||||||
|
Bundle size (minified + gzipped):
|
||||||
|
Valibot: 8KB ← Smallest
|
||||||
|
ArkType: 12KB
|
||||||
|
TypeBox: 60KB (includes JSON Schema types)
|
||||||
|
Zod v4: 60KB
|
||||||
|
|
||||||
|
Type inference speed (tsc, 50-field schema):
|
||||||
|
ArkType: ~200ms
|
||||||
|
Zod v4: ~450ms
|
||||||
|
Valibot: ~600ms
|
||||||
|
TypeBox: ~300ms
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Zod v4: The Default](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026\#zod-v4-the-default)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Zod v4 — new features and performance improvements:
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Basic schema (same as v3):
|
||||||
|
const UserSchema = z.object({
|
||||||
|
id: z.string().cuid2(),
|
||||||
|
email: z.string().email(),
|
||||||
|
name: z.string().min(2).max(100),
|
||||||
|
age: z.number().int().min(0).max(150).optional(),
|
||||||
|
role: z.enum(['user', 'admin', 'moderator']),
|
||||||
|
tags: z.array(z.string()).max(10),
|
||||||
|
address: z.object({
|
||||||
|
street: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
country: z.string().length(2), // ISO 2-letter
|
||||||
|
}).optional(),
|
||||||
|
metadata: z.record(z.string(), z.unknown()),
|
||||||
|
createdAt: z.coerce.date(), // Auto-coerce string → Date
|
||||||
|
});
|
||||||
|
|
||||||
|
type User = z.infer<typeof UserSchema>;
|
||||||
|
|
||||||
|
// Zod v4 new: z.file() for Blob/File
|
||||||
|
const UploadSchema = z.object({
|
||||||
|
file: z.instanceof(File)
|
||||||
|
.refine(f => f.size < 5_000_000, 'Max 5MB')
|
||||||
|
.refine(f => ['image/jpeg', 'image/png', 'image/webp'].includes(f.type), 'Must be JPEG/PNG/WebP'),
|
||||||
|
caption: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zod v4 new: z.pipe() for chained transforms
|
||||||
|
const ParsedDateSchema = z
|
||||||
|
.string()
|
||||||
|
.pipe(z.coerce.date()); // string → validated Date
|
||||||
|
|
||||||
|
// Zod v4 new: z.toJSONSchema()
|
||||||
|
const jsonSchema = z.toJSONSchema(UserSchema);
|
||||||
|
// Generates standard JSON Schema — useful for OpenAPI docs
|
||||||
|
|
||||||
|
// Error formatting (v4 — cleaner):
|
||||||
|
const result = UserSchema.safeParse({ email: 'bad' });
|
||||||
|
if (!result.success) {
|
||||||
|
const errors = result.error.flatten();
|
||||||
|
// { fieldErrors: { email: ['Invalid email'] }, formErrors: [] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Valibot: Bundle-Size Champion](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026\#valibot-bundle-size-champion)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Valibot — modular, tree-shakable:
|
||||||
|
import {
|
||||||
|
object, string, number, array, optional, enum_,
|
||||||
|
email, minLength, maxLength, integer, minValue, maxValue,
|
||||||
|
parse, safeParse, flatten,
|
||||||
|
type InferInput, type InferOutput,
|
||||||
|
} from 'valibot';
|
||||||
|
|
||||||
|
// Only imports what you use — tree-shaking reduces bundle to ~2-5KB for simple schemas
|
||||||
|
const UserSchema = object({
|
||||||
|
id: string([minLength(1)]),
|
||||||
|
email: string([email()]),
|
||||||
|
name: string([minLength(2), maxLength(100)]),
|
||||||
|
age: optional(number([integer(), minValue(0), maxValue(150)])),
|
||||||
|
role: enum_(['user', 'admin', 'moderator']),
|
||||||
|
tags: array(string(), [maxLength(10)]),
|
||||||
|
});
|
||||||
|
|
||||||
|
type User = InferInput<typeof UserSchema>;
|
||||||
|
|
||||||
|
// Parse (throws on error):
|
||||||
|
const user = parse(UserSchema, rawData);
|
||||||
|
|
||||||
|
// Safe parse (returns result/error):
|
||||||
|
const result = safeParse(UserSchema, rawData);
|
||||||
|
if (result.success) {
|
||||||
|
console.log(result.output);
|
||||||
|
} else {
|
||||||
|
const errors = flatten(result.issues);
|
||||||
|
// { nested: { email: ['Invalid email'] } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Valibot with React Hook Form:
|
||||||
|
import { valibotResolver } from '@hookform/resolvers/valibot';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
function SignupForm() {
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm({
|
||||||
|
resolver: valibotResolver(UserSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(console.log)}>
|
||||||
|
<input {...register('email')} />
|
||||||
|
{errors.email && <span>{errors.email.message}</span>}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [ArkType: Fastest Runtime + TypeScript Syntax](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026\#arktype-fastest-runtime--typescript-syntax)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ArkType — TypeScript-syntax strings for schemas:
|
||||||
|
import { type } from 'arktype';
|
||||||
|
|
||||||
|
// Syntax feels like writing TypeScript:
|
||||||
|
const User = type({
|
||||||
|
id: 'string',
|
||||||
|
email: 'string.email',
|
||||||
|
name: '2 <= string <= 100', // min/max length shorthand!
|
||||||
|
age: 'number.integer | undefined',
|
||||||
|
role: '"user" | "admin" | "moderator"',
|
||||||
|
tags: 'string[] <= 10', // array with max length
|
||||||
|
createdAt: 'Date',
|
||||||
|
});
|
||||||
|
|
||||||
|
type User = typeof User.infer;
|
||||||
|
|
||||||
|
// Parse:
|
||||||
|
const result = User(rawData);
|
||||||
|
|
||||||
|
// ArkType returns morph (with parse) or error:
|
||||||
|
if (result instanceof type.errors) {
|
||||||
|
console.log(result.summary); // Human-readable error
|
||||||
|
} else {
|
||||||
|
// result is User
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArkType advanced: morphs (transform)
|
||||||
|
const ParsedDate = type('string').pipe(s => new Date(s), 'Date');
|
||||||
|
|
||||||
|
// Recursive types (Zod struggles here):
|
||||||
|
const TreeNode = type({
|
||||||
|
value: 'number',
|
||||||
|
children: 'TreeNode[]', // Self-referencing!
|
||||||
|
}).describe('TreeNode'); // Named for error messages
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [TypeBox: JSON Schema Native](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026\#typebox-json-schema-native)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// TypeBox — generates JSON Schema, used in Fastify/Hono:
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { Value } from '@sinclair/typebox/value';
|
||||||
|
|
||||||
|
// TypeBox schema IS JSON Schema:
|
||||||
|
const UserSchema = Type.Object({
|
||||||
|
id: Type.String({ format: 'uuid' }),
|
||||||
|
email: Type.String({ format: 'email' }),
|
||||||
|
name: Type.String({ minLength: 2, maxLength: 100 }),
|
||||||
|
age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),
|
||||||
|
role: Type.Union([\
|
||||||
|
Type.Literal('user'),\
|
||||||
|
Type.Literal('admin'),\
|
||||||
|
Type.Literal('moderator'),\
|
||||||
|
]),
|
||||||
|
tags: Type.Array(Type.String(), { maxItems: 10 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// TypeScript type from schema:
|
||||||
|
type User = Static<typeof UserSchema>;
|
||||||
|
|
||||||
|
// Validate:
|
||||||
|
const result = Value.Check(UserSchema, rawData);
|
||||||
|
if (!result) {
|
||||||
|
const errors = [...Value.Errors(UserSchema, rawData)];
|
||||||
|
// [{ path: '/email', message: 'Expected string' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeBox + Hono (validated routes with OpenAPI):
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import { describeRoute } from 'hono-openapi';
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
app.post('/users',
|
||||||
|
describeRoute({
|
||||||
|
requestBody: { content: { 'application/json': { schema: UserSchema } } },
|
||||||
|
responses: { 201: { description: 'Created' } },
|
||||||
|
}),
|
||||||
|
async (c) => { /* handler */ }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Export OpenAPI spec:
|
||||||
|
// app.doc('/openapi.json', { openapi: '3.0.0', info: { title: 'API', version: '1' } })
|
||||||
|
```
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## [Decision Guide](https://www.pkgpulse.com/blog/zod-v4-vs-arktype-vs-typebox-vs-valibot-schema-2026\#decision-guide)
|
||||||
|
|
||||||
|
```
|
||||||
|
Use Zod v4 if:
|
||||||
|
→ Default choice — best ecosystem (react-hook-form, trpc, drizzle, next-safe-action)
|
||||||
|
→ Team already knows Zod v3 (v4 is mostly backwards compatible)
|
||||||
|
→ Need broad library compatibility
|
||||||
|
→ Bundle size is not a constraint
|
||||||
|
|
||||||
|
Use Valibot if:
|
||||||
|
→ Edge runtime / bundle size critical (<5KB budget)
|
||||||
|
→ Want tree-shakable, pay-only-for-what-you-use
|
||||||
|
→ Cloudflare Workers or similar constrained environments
|
||||||
|
|
||||||
|
Use ArkType if:
|
||||||
|
→ Parsing millions of objects (backend hot path)
|
||||||
|
→ Love TypeScript-native syntax strings
|
||||||
|
→ Need recursive types easily
|
||||||
|
→ Fastest possible validation
|
||||||
|
|
||||||
|
Use TypeBox if:
|
||||||
|
→ Building OpenAPI/Swagger documentation
|
||||||
|
→ Using Fastify (TypeBox is Fastify's native schema)
|
||||||
|
→ Need JSON Schema output for other tools
|
||||||
|
→ API validation that also generates docs
|
||||||
|
```
|
||||||
|
|
||||||
|
_Compare Zod, Valibot, ArkType, and TypeBox on [PkgPulse](https://pkgpulse.com/compare/zod-vs-valibot)._
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
### The 2026 JavaScript Stack Cheatsheet
|
||||||
|
|
||||||
|
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
|
||||||
|
|
||||||
|
Get the Free Cheatsheet
|
||||||
252
.firecrawl/shadcn-claude-settings.md
Normal file
252
.firecrawl/shadcn-claude-settings.md
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
## TL;DR
|
||||||
|
|
||||||
|
Claude Code guesses your component APIs, theme tokens, and project setup — and gets them wrong half the time. shadcn/ui ships 3 tools (Skills, MCP Server, Preset) that inject real project context into your AI. Setup takes 5 minutes, 3 commands.
|
||||||
|
|
||||||
|
```
|
||||||
|
# 1. Project context
|
||||||
|
npx skills add shadcn/ui
|
||||||
|
|
||||||
|
# 2. Live docs
|
||||||
|
claude mcp add shadcn -- npx shadcn@latest mcp
|
||||||
|
|
||||||
|
# 3. Design system
|
||||||
|
npx shadcn@latest init --preset a1Dg5eFl
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter fullscreen modeExit fullscreen mode
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## The Problem: AI-Generated UI Looks Inconsistent
|
||||||
|
|
||||||
|
If you've used Claude Code to build UI, you've probably seen this:
|
||||||
|
|
||||||
|
- The AI generates a `<Button variant="outline">` but your project uses `<Button variant="ghost">`.
|
||||||
|
- Colors change between pages because there's no shared design token.
|
||||||
|
- You spend 15 minutes searching docs for the correct props, then paste them into the prompt.
|
||||||
|
|
||||||
|
This happens because Claude Code doesn't know your project context. It relies on training data — not your `components.json`, not your Tailwind config, not your installed component list.
|
||||||
|
|
||||||
|
## What is shadcn/ui?
|
||||||
|
|
||||||
|
Quick background if you haven't used it: **shadcn/ui** is a copy-and-own component system built on Radix UI and Tailwind CSS. Instead of installing an npm package, you copy component source code directly into your project. Full ownership, full customization.
|
||||||
|
|
||||||
|
86,900+ GitHub stars. Used by Adobe, OpenAI, Sonos. 70+ components — Button, Card, Dialog, Data Table, Sidebar, Chart, and more.
|
||||||
|
|
||||||
|
## The 3 Settings That Fix Everything
|
||||||
|
|
||||||
|
shadcn/ui CLI v4 (released March 2026) has a tagline: **"Built for you and your coding agents."** Here's what that means in practice.
|
||||||
|
|
||||||
|
### 1\. Skills: Inject Project Context
|
||||||
|
|
||||||
|
```
|
||||||
|
npx skills add shadcn/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter fullscreen modeExit fullscreen mode
|
||||||
|
|
||||||
|
This one command makes Claude Code "understand" your project. Skills detects your `components.json` and runs `shadcn info --json` to feed the AI:
|
||||||
|
|
||||||
|
- Framework type and version
|
||||||
|
- Tailwind CSS version and config
|
||||||
|
- Path aliases
|
||||||
|
- Installed component list
|
||||||
|
- Icon library info
|
||||||
|
|
||||||
|
Before Skills, the AI guesses. After Skills, it reads your actual setup and generates code that matches — correct FieldGroup patterns, correct semantic color variables, correct import paths.
|
||||||
|
|
||||||
|
### 2\. MCP Server: Live Documentation Access
|
||||||
|
|
||||||
|
```
|
||||||
|
claude mcp add shadcn -- npx shadcn@latest mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter fullscreen modeExit fullscreen mode
|
||||||
|
|
||||||
|
MCP (Model Context Protocol) connects Claude Code directly to the shadcn/ui registry. During a conversation, the AI can:
|
||||||
|
|
||||||
|
- Look up component docs, examples, and props in real time
|
||||||
|
- Auto-generate install commands
|
||||||
|
- Search community registries
|
||||||
|
|
||||||
|
No more browser tabs. Ask "add sorting to my Data Table" and Claude Code pulls the latest docs, then writes the correct implementation.
|
||||||
|
|
||||||
|
### 3\. Preset: One-Line Design System
|
||||||
|
|
||||||
|
```
|
||||||
|
npx shadcn@latest init --preset a1Dg5eFl
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter fullscreen modeExit fullscreen mode
|
||||||
|
|
||||||
|
A Preset packs colors, themes, icons, fonts, and border-radius into a single code. Build your design system visually at [shadcn/create](https://ui.shadcn.com/create), share the preset code with your team, and everyone — including Claude Code — uses the same design tokens.
|
||||||
|
|
||||||
|
Include the preset code in your AI prompts, and every generated component stays consistent with your design system.
|
||||||
|
|
||||||
|
## Before vs After
|
||||||
|
|
||||||
|
| | Before | After |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Context | Guesses from training data | Reads live project config |
|
||||||
|
| API accuracy | Frequent errors | Correct patterns from latest docs |
|
||||||
|
| Theme consistency | Different per page | Unified via CSS variables & preset |
|
||||||
|
| Doc lookup | Manual, ~15 min | MCP auto-search |
|
||||||
|
| Component scope | Basic UI only | Includes community registries |
|
||||||
|
|
||||||
|
## CLI v4 Commands You'll Actually Use
|
||||||
|
|
||||||
|
```
|
||||||
|
# Initialize with preset
|
||||||
|
npx shadcn@latest init --preset [CODE]
|
||||||
|
|
||||||
|
# Add components
|
||||||
|
npx shadcn@latest add button card dialog
|
||||||
|
|
||||||
|
# Install everything
|
||||||
|
npx shadcn@latest add --all
|
||||||
|
|
||||||
|
# Search registries
|
||||||
|
npx shadcn@latest search @shadcn
|
||||||
|
|
||||||
|
# Preview components
|
||||||
|
npx shadcn@latest view button card
|
||||||
|
|
||||||
|
# Look up docs
|
||||||
|
npx shadcn@latest docs data-table
|
||||||
|
|
||||||
|
# Dry run (preview changes)
|
||||||
|
npx shadcn@latest add --dry-run
|
||||||
|
|
||||||
|
# Show diff before applying
|
||||||
|
npx shadcn@latest add --diff
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter fullscreen modeExit fullscreen mode
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
**Is shadcn/ui free?**
|
||||||
|
|
||||||
|
Yes. Fully open source, MIT license. Commercial use is fine.
|
||||||
|
|
||||||
|
**Does it work outside Next.js?**
|
||||||
|
|
||||||
|
CLI v4 supports Next.js, Remix, Vite, Astro, and more. It auto-detects your framework from `components.json`.
|
||||||
|
|
||||||
|
**Do I need all 3 settings?**
|
||||||
|
|
||||||
|
Skills alone gives you project context. But adding MCP Server (live docs) and Preset (design tokens) together gives the best results. The 3 tools complement each other.
|
||||||
|
|
||||||
|
**Can I add this to an existing project?**
|
||||||
|
|
||||||
|
Yes. If `components.json` exists, Skills picks it up automatically. For new projects, start with `npx shadcn@latest init`.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Wrapping Up
|
||||||
|
|
||||||
|
3 commands, 5 minutes. Your AI stops guessing and starts reading your actual project. If you haven't set this up yet, start with `npx skills add shadcn/ui` — you'll see the difference on the first prompt.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [shadcn/ui CLI docs](https://ui.shadcn.com/docs/cli) — Full CLI v4 reference
|
||||||
|
- [shadcn/ui Skills docs](https://ui.shadcn.com/docs/skills) — AI agent Skills setup
|
||||||
|
- [shadcn/ui MCP Server docs](https://ui.shadcn.com/docs/registry/mcp) — MCP connection guide
|
||||||
|
- [shadcn/ui Changelog: CLI v4](https://ui.shadcn.com/docs/changelog/2026-03-cli-v4) — v4 release notes
|
||||||
|
|
||||||
|
[\\
|
||||||
|
Sonar](https://dev.to/sonar) Promoted
|
||||||
|
|
||||||
|
Dropdown menu
|
||||||
|
|
||||||
|
- [What's a billboard?](https://dev.to/billboards)
|
||||||
|
- [Manage preferences](https://dev.to/settings/customization#sponsors)
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
- [Report billboard](https://dev.to/report-abuse?billboard=259979)
|
||||||
|
|
||||||
|
[](https://www.sonarsource.com/sem/the-state-of-code/developer-survey-report/?utm_medium=paid&utm_source=dev&utm_campaign=ss-state-of-code-developer-survey26&utm_content=report-devsurvey-banner-x-2&utm_term=ww-all-x&s_category=Paid&s_source=Paid+Social&s_origin=dev&bb=259979)
|
||||||
|
|
||||||
|
## [State of Code Developer Survey report](https://www.sonarsource.com/sem/the-state-of-code/developer-survey-report/?utm_medium=paid&utm_source=dev&utm_campaign=ss-state-of-code-developer-survey26&utm_content=report-devsurvey-banner-x-2&utm_term=ww-all-x&s_category=Paid&s_source=Paid+Social&s_origin=dev&bb=259979)
|
||||||
|
|
||||||
|
Did you know 96% of developers don't fully trust that AI-generated code is functionally correct, yet only 48% always check it before committing? Check out Sonar's new report on the real-world impact of AI on development teams.
|
||||||
|
|
||||||
|
[Read the results](https://www.sonarsource.com/sem/the-state-of-code/developer-survey-report/?utm_medium=paid&utm_source=dev&utm_campaign=ss-state-of-code-developer-survey26&utm_content=report-devsurvey-banner-x-2&utm_term=ww-all-x&s_category=Paid&s_source=Paid+Social&s_origin=dev&bb=259979)
|
||||||
|
|
||||||
|
Read More
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[Create template](https://dev.to/settings/response-templates)
|
||||||
|
|
||||||
|
Templates let you quickly answer FAQs or store snippets for re-use.
|
||||||
|
|
||||||
|
SubmitPreview [Dismiss](https://dev.to/404.html)
|
||||||
|
|
||||||
|
Some comments may only be visible to logged-in visitors. [Sign in](https://dev.to/enter) to view all comments.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's [permalink](https://dev.to/_46ea277e677b888e0cd13/shadcnui-claude-code-3-settings-that-fix-ai-generated-ui-quality-2dea#).
|
||||||
|
|
||||||
|
|
||||||
|
Hide child comments as well
|
||||||
|
|
||||||
|
Confirm
|
||||||
|
|
||||||
|
|
||||||
|
For further actions, you may consider blocking this person and/or [reporting abuse](https://dev.to/report-abuse)
|
||||||
|
|
||||||
|
[\\
|
||||||
|
The DEV Team](https://dev.to/devteam) Promoted
|
||||||
|
|
||||||
|
Dropdown menu
|
||||||
|
|
||||||
|
- [What's a billboard?](https://dev.to/billboards)
|
||||||
|
- [Manage preferences](https://dev.to/settings/customization#sponsors)
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
- [Report billboard](https://dev.to/report-abuse?billboard=262076)
|
||||||
|
|
||||||
|
[](https://dev.to/googleai/gemini-31-flash-lite-developer-guide-and-use-cases-1hh?bb=262076)
|
||||||
|
|
||||||
|
## [Gemini 3.1 Flash-Lite: Developer guide and use cases](https://dev.to/googleai/gemini-31-flash-lite-developer-guide-and-use-cases-1hh?bb=262076)
|
||||||
|
|
||||||
|
Gemini 3.1 Flash-Lite is the high-volume, affordable powerhouse of the Gemini family. It’s purpose-built for large-scale tasks where speed and cost-efficiency are the main priorities, making it the ideal engine for background processing. Whether you're handling a constant stream of user interactions or need to process massive datasets with tasks like translation, transcription, or extraction, Flash-Lite provides the optimal balance of speed and capability.
|
||||||
|
|
||||||
|
This guide walks through seven practical use cases for Flash-Lite using the google-genai Python SDK.
|
||||||
|
|
||||||
|
[Read More](https://dev.to/googleai/gemini-31-flash-lite-developer-guide-and-use-cases-1hh?bb=262076)
|
||||||
|
|
||||||
|
👋 Kindness is contagious
|
||||||
|
|
||||||
|
Dropdown menu
|
||||||
|
|
||||||
|
- [What's a billboard?](https://dev.to/billboards)
|
||||||
|
- [Manage preferences](https://dev.to/settings/customization#sponsors)
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
- [Report billboard](https://dev.to/report-abuse?billboard=236872)
|
||||||
|
|
||||||
|
x
|
||||||
|
|
||||||
|
Discover fresh viewpoints in this insightful post, supported by our vibrant DEV Community. **Every developer’s experience matters**—add your thoughts and help us grow together.
|
||||||
|
|
||||||
|
A simple “thank you” can uplift the author and spark new discussions—leave yours below!
|
||||||
|
|
||||||
|
On DEV, **knowledge-sharing connects us and drives innovation**. Found this useful? A quick note of appreciation makes a real impact.
|
||||||
|
|
||||||
|
## [Okay](https://dev.to/enter?state=new-user&bb=236872)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
We're a place where coders share, stay up-to-date and grow their careers.
|
||||||
|
|
||||||
|
|
||||||
|
[Log in](https://dev.to/enter?signup_subforem=1) [Create account](https://dev.to/enter?signup_subforem=1&state=new-user)
|
||||||
|
|
||||||
|

|
||||||
149
.firecrawl/shadcn-why-ai-loves.md
Normal file
149
.firecrawl/shadcn-why-ai-loves.md
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
[Stop Rebuilding UI From ScratchStop Rebuilding UI](https://www.shadcn.io/pricing)
|
||||||
|
|
||||||
|
Shadcn.io is not affiliated with official [shadcn/ui](https://ui.shadcn.com/)
|
||||||
|
|
||||||
|
[Previous: Shadcn UI React Components](https://www.shadcn.io/ui) Ask AI
|
||||||
|
|
||||||
|
[Discord](https://discord.gg/Z9NVtNE7bj "Join Discord") [GitHub](https://github.com/shadcnio/react-shadcn-components "View on GitHub") [Next: Shadcn Installation Guide](https://www.shadcn.io/ui/installation-guide)
|
||||||
|
|
||||||
|
# Why AI Coding Tools Love Shadcn UI
|
||||||
|
|
||||||
|
Why shadcn UI works perfectly with AI coding tools like Cursor, Copilot, and v0. Copy-paste React components with TypeScript for Next.js applications.
|
||||||
|
|
||||||
|
Table of Contents
|
||||||
|
|
||||||
|
# [Why AI Coding Tools Love React Components You Actually Own](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#why-ai-coding-tools-love-react-components-you-actually-own)
|
||||||
|
|
||||||
|
Building React applications with AI coding tools in 2024 means your AI assistant needs to understand your components. Ask it to "make the button green and add a loading spinner" with most UI libraries, and your AI basically throws up its hands. "Well, you could try overriding the theme... or maybe use a CSS class... actually, let me check the docs..." Sound familiar?
|
||||||
|
|
||||||
|
But with shadcn/ui React components? Your AI just opens up `components/ui/button.tsx`, sees exactly how everything works with TypeScript, changes `bg-blue-500` to `bg-green-500`, tosses in a spinner, updates the props interface. Boom. Done.
|
||||||
|
|
||||||
|
Here's what nobody talks about: the best React component library for AI coding tools isn't the one with the most features—it's the one your AI can actually read and modify with TypeScript.
|
||||||
|
|
||||||
|
## [The black box problem we've all lived with](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#the-black-box-problem-weve-all-lived-with)
|
||||||
|
|
||||||
|
Look, we've all been there. You're using Material UI or Chakra in your Next.js application, and you want to make one tiny change to a React component. You end up in this rabbit hole of theme providers, style overrides, and documentation diving. It's frustrating enough when you're doing it yourself.
|
||||||
|
|
||||||
|
Now imagine your AI coding tool trying to help. It's basically playing blind chess. All the actual component logic is compiled and hidden away in `node_modules`. Your AI can see the public API, sure, but it has no clue how anything actually works under the hood with TypeScript.
|
||||||
|
|
||||||
|
Want to modify a button's behavior? Your AI starts guessing: "Maybe try the theme object? Or this CSS-in-JS prop? Actually, let me search the docs..." Meanwhile, you're sitting there thinking, "Just change the damn button."
|
||||||
|
|
||||||
|
## [shadcn/ui flipped the script](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#shadcnui-flipped-the-script)
|
||||||
|
|
||||||
|
Here's what shadcn/ui did differently. Instead of giving you another black box library, it just... gives you the code. Seriously, that's it.
|
||||||
|
|
||||||
|
Run `npx shadcn@latest add button` and boom—you've got a `button.tsx` file with TypeScript sitting right there in your Next.js codebase. Your AI coding tool can read it like any other React component in your project. No mysteries, no abstractions, no "the magic happens elsewhere."
|
||||||
|
|
||||||
|
And here's the kicker: it's all built with Tailwind CSS. Classes like `bg-primary hover:bg-primary/90` tell the whole story. Your AI doesn't need to decode some complex theme system—it can literally see that hovering makes the background 90% opacity.
|
||||||
|
|
||||||
|
Want a loading state? Your AI looks at the component, sees how variants work, and just... adds one. No theme provider wrestling, no API limitations. Just code doing what code does.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Before: Simple button
|
||||||
|
<Button variant="default">Click me</Button>
|
||||||
|
|
||||||
|
// After: AI adds loading state by reading the component
|
||||||
|
<Button variant="default" loading={isLoading}>
|
||||||
|
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||||
|
Click me
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## [Why AI gets shadcn/ui so well](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#why-ai-gets-shadcnui-so-well)
|
||||||
|
|
||||||
|
Think about it—AI is really good at pattern recognition and code modification. But it's only as good as what it can see.
|
||||||
|
|
||||||
|
With Tailwind CSS, everything's explicit in your React components. `text-blue-500` always means the same thing. Your AI coding tool reads `hover:bg-blue-600 focus:ring-2 focus:ring-blue-500` and immediately knows what's going on. No theme system to decode, no CSS-in-JS abstractions to parse in your TypeScript files.
|
||||||
|
|
||||||
|
After your AI sees a couple shadcn/ui React components, it starts picking up on the patterns. How variants work with TypeScript, how you structure Next.js layouts, even your accessibility patterns. It's like having a junior dev who learns your coding style really, really fast.
|
||||||
|
|
||||||
|
The difference? With traditional libraries, all these patterns are hidden in theme configs and style overrides. With shadcn/ui, they're right there in the component files where your AI can see them.
|
||||||
|
|
||||||
|
```
|
||||||
|
// What your AI sees in button.tsx
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md text-sm font-medium",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline: "border border-input bg-background hover:bg-accent",
|
||||||
|
// AI can easily add new variants here
|
||||||
|
loading: "bg-primary/50 text-primary-foreground cursor-not-allowed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## [What this actually means for your workflow](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#what-this-actually-means-for-your-workflow)
|
||||||
|
|
||||||
|
Here's the crazy part: shadcn/ui wasn't designed for AI at all. It was designed to solve the developer control problem. But in doing that, it accidentally solved the AI problem too.
|
||||||
|
|
||||||
|
Your AI coding tool doesn't need docs anymore—it just reads your React components with TypeScript. When it suggests changes, they actually work because it understands the real implementation in your Next.js application, not some abstract API description. And the more you customize your components, the better your AI gets at understanding your specific style.
|
||||||
|
|
||||||
|
## [The bigger pattern emerging](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#the-bigger-pattern-emerging)
|
||||||
|
|
||||||
|
You know what's interesting? This transparency thing isn't just winning in React and Next.js development. Look around—AI coding tools work better with anything they can actually read and understand.
|
||||||
|
|
||||||
|
Terraform configs vs complex deployment abstractions. Raw SQL schemas vs ORM magic. OpenAPI specs vs undocumented REST APIs. TypeScript interfaces vs any types. Every time, the transparent, declarative approach wins when AI gets involved.
|
||||||
|
|
||||||
|
shadcn/ui just happened to stumble onto this principle first in the React component library space. But the pattern is clear: if you want to build something that works well with AI coding tools, make it readable with TypeScript, make it explicit, and put it where AI can see it in your Next.js codebase.
|
||||||
|
|
||||||
|
## [What comes next](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#what-comes-next)
|
||||||
|
|
||||||
|
We're still in the early days, but the trajectory is clear. As AI coding tools get better at understanding React component patterns with TypeScript, they amplify shadcn/ui's transparency advantage. More Next.js projects adopt these patterns, creating better training data, which makes AI tools even more effective. It's a virtuous cycle.
|
||||||
|
|
||||||
|
We're not just building better UIs anymore. We're building UIs that think.
|
||||||
|
|
||||||
|
## [The simple truth](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#the-simple-truth)
|
||||||
|
|
||||||
|
Here's the beautiful part: you don't need to change anything. Use shadcn/ui React components with TypeScript exactly as you normally would in your Next.js application. The AI compatibility comes from the transparency, not special APIs or patterns.
|
||||||
|
|
||||||
|
shadcn/ui accidentally solved the AI coding tool compatibility problem by solving the developer control problem. When you own your React component code with TypeScript, both you and your AI can understand it. The result? A development experience that feels like having a pair programmer who actually understands your Next.js codebase.
|
||||||
|
|
||||||
|
## [AI-Friendly Components to Try](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#ai-friendly-components-to-try)
|
||||||
|
|
||||||
|
These React components work exceptionally well with AI coding tools because of their transparent, readable structure:
|
||||||
|
|
||||||
|
[**Button** \\
|
||||||
|
Variants and states that AI can easily read and modify](https://www.shadcn.io/ui/button) [**Input** \\
|
||||||
|
Form components with clear TypeScript interfaces](https://www.shadcn.io/ui/input) [**Dialog** \\
|
||||||
|
Complex components AI can understand and customize](https://www.shadcn.io/ui/dialog) [**Form** \\
|
||||||
|
Validation patterns AI can replicate and extend](https://www.shadcn.io/ui/form) [**Data Table** \\
|
||||||
|
Advanced components with clear sorting and filtering logic](https://www.shadcn.io/ui/data-table) [**Command** \\
|
||||||
|
Search interfaces AI can enhance with new features](https://www.shadcn.io/ui/command)
|
||||||
|
|
||||||
|
## [Ready to try it yourself?](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#ready-to-try-it-yourself)
|
||||||
|
|
||||||
|
Pick your framework and get started with shadcn/ui. The [installation guide](https://www.shadcn.io/ui/installation-guide) walks you through setup for Next.js, Vite, Remix, and more. Takes about 5 minutes to get your first component working.
|
||||||
|
|
||||||
|
Once you're set up, here's where to go next:
|
||||||
|
|
||||||
|
1. Browse [official components](https://www.shadcn.io/ui) for forms, tables, and UI elements
|
||||||
|
2. Add [charts](https://www.shadcn.io/charts) for data visualization
|
||||||
|
3. Explore [community components](https://www.shadcn.io/components) for extended functionality
|
||||||
|
4. Add useful [hooks](https://www.shadcn.io/hooks) to enhance your components
|
||||||
|
5. Use pre-built [blocks](https://www.shadcn.io/blocks) to quickly build common layouts
|
||||||
|
|
||||||
|
Then ask your AI assistant to modify something. Watch it actually understand your components instead of guessing from docs. You'll never want to go back to black box libraries again.
|
||||||
|
|
||||||
|
## [Questions you're probably thinking](https://www.shadcn.io/ui/why-ai-coding-tools-love-shadcn-ui\#questions-youre-probably-thinking)
|
||||||
|
|
||||||
|
### Do I need to learn new patterns for AI-assisted development?
|
||||||
|
|
||||||
|
### Will this work with future AI coding tools?
|
||||||
|
|
||||||
|
### Are other libraries adopting this approach?
|
||||||
|
|
||||||
|
Was this page helpful?
|
||||||
|
|
||||||
|
[Sign in](https://www.shadcn.io/sign-in) to leave feedback.
|
||||||
|
|
||||||
|
[Shadcn UI React Components\\
|
||||||
|
\\
|
||||||
|
Copy-paste React components built with Radix UI and Tailwind CSS. Open source component library for Next.js with TypeScript support and full code ownership.](https://www.shadcn.io/ui) [Shadcn Installation Guide\\
|
||||||
|
\\
|
||||||
|
Setup guides for shadcn/ui with Next.js, Vite, Remix, Laravel, Astro, and more React frameworks. Get started with TypeScript components quickly.](https://www.shadcn.io/ui/installation-guide)
|
||||||
151
.firecrawl/solid-primitives-context.md
Normal file
151
.firecrawl/solid-primitives-context.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
Context
|
||||||
|
|
||||||
|
# Context
|
||||||
|
|
||||||
|
Context
|
||||||
|
|
||||||
|
Context
|
||||||
|
|
||||||
|
Context
|
||||||
|
|
||||||
|
Size
|
||||||
|
|
||||||
|
263 B
|
||||||
|
[NPM\\
|
||||||
|
\\
|
||||||
|
v0.3.1](https://www.npmjs.com/package//@solid-primitives/context)
|
||||||
|
Stage
|
||||||
|
|
||||||
|
2
|
||||||
|
|
||||||
|
## [\#](https://primitives.solidjs.community/package/context/\#installation) Installation
|
||||||
|
|
||||||
|
Copy
|
||||||
|
|
||||||
|
npm install @solid-primitives/context
|
||||||
|
|
||||||
|
Copy
|
||||||
|
|
||||||
|
yarn add @solid-primitives/context
|
||||||
|
|
||||||
|
Copy
|
||||||
|
|
||||||
|
pnpm add @solid-primitives/context
|
||||||
|
|
||||||
|
## [\#](https://primitives.solidjs.community/package/context/\#readme) Readme
|
||||||
|
|
||||||
|
Primitives simplifying the creation and use of SolidJS Context API.
|
||||||
|
|
||||||
|
- [`createContextProvider`](https://primitives.solidjs.community/package/context/#createcontextprovider) \- Create the Context Provider component and useContext function with types inferred from the factory function.
|
||||||
|
- [`MultiProvider`](https://primitives.solidjs.community/package/context/#multiprovider) \- A component that allows you to provide multiple contexts at once.
|
||||||
|
|
||||||
|
## [\#](https://primitives.solidjs.community/package/context/\#createcontextprovider)`createContextProvider`
|
||||||
|
|
||||||
|
Create the Context Provider component and useContext function with types inferred from the factory function.
|
||||||
|
|
||||||
|
### [\#](https://primitives.solidjs.community/package/context/\#how-to-use-it) How to use it
|
||||||
|
|
||||||
|
Given a factory function, `createContextProvider` creates a SolidJS Context and returns both a Provider component for setting the context, and a useContext helper for getting the context. The factory function gets called when the provider component gets executed; all `props` of the provider component get passed into the factory function, and what it returns will be available in the contexts for all the underlying components. The types of the provider props and context are inferred from the factory function.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createContextProvider } from "@solid-primitives/context";
|
||||||
|
|
||||||
|
const [CounterProvider, useCounter] = createContextProvider((props: { initial: number }) => {
|
||||||
|
const [count, setCount] = createSignal(props.initial);
|
||||||
|
const increment = () => setCount(count() + 1);
|
||||||
|
return { count, increment };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Provide the context
|
||||||
|
<CounterProvider initial={1}>
|
||||||
|
<App />
|
||||||
|
</CounterProvider>;
|
||||||
|
|
||||||
|
// Use the context in a child component
|
||||||
|
const ctx = useCounter();
|
||||||
|
ctx; // T: { count: () => number; increment: () => void; } | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
### [\#](https://primitives.solidjs.community/package/context/\#providing-context-fallback) Providing context fallback
|
||||||
|
|
||||||
|
The `createContextProvider` primitive takes a second, optional argument for providing context defaults for when the context wouldn't be provided higher in the component tree.
|
||||||
|
Providing a fallback also removes `undefined` from `T | undefined` return type of the `useContext` function.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [CounterProvider, useCounter] = createContextProvider(
|
||||||
|
() => {
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
const increment = () => setCount(count() + 1);
|
||||||
|
return { count, increment };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: () => 0,
|
||||||
|
increment: () => {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// then when using the context:
|
||||||
|
const { count } = useCounter();
|
||||||
|
```
|
||||||
|
|
||||||
|
Definite context types without defaults:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const useDefiniteCounter = () => useCounter()!;
|
||||||
|
```
|
||||||
|
|
||||||
|
### [\#](https://primitives.solidjs.community/package/context/\#demo) Demo
|
||||||
|
|
||||||
|
[https://codesandbox.io/s/solid-primitives-context-demo-oqyie2?file=/index.tsx](https://codesandbox.io/s/solid-primitives-context-demo-oqyie2?file=/index.tsx)
|
||||||
|
|
||||||
|
## [\#](https://primitives.solidjs.community/package/context/\#multiprovider)`MultiProvider`
|
||||||
|
|
||||||
|
A component that allows you to provide multiple contexts at once.
|
||||||
|
|
||||||
|
It will work exactly like nesting multiple providers as separate components, but it will save you from the nesting.
|
||||||
|
|
||||||
|
### [\#](https://primitives.solidjs.community/package/context/\#how-to-use-it-1) How to use it
|
||||||
|
|
||||||
|
`MultiProvider` takes only a single `values` with a key-value pair of the context and the value to provide.
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> Values list is evaluated in order, so the context values will be provided in the same way as if you were nesting the providers.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { MultiProvider } from "@solid-primitives/context";
|
||||||
|
|
||||||
|
// before
|
||||||
|
<FooContext.Provider value={"foo"}>
|
||||||
|
<BarContext.Provider value={"bar"}>
|
||||||
|
<BazContext.Provider value={"baz"}>
|
||||||
|
<MyCustomProviderComponent value={"hello-world"}>
|
||||||
|
<BoundContextProvider>
|
||||||
|
<App />
|
||||||
|
</BoundContextProvider>
|
||||||
|
</MyCustomProviderComponent>
|
||||||
|
</BazContext.Provider>
|
||||||
|
</BarContext.Provider>
|
||||||
|
</FooContext.Provider>;
|
||||||
|
|
||||||
|
// after
|
||||||
|
<MultiProvider
|
||||||
|
values={[\
|
||||||
|
[FooContext, "foo"],\
|
||||||
|
[BarContext, "bar"],\
|
||||||
|
[BazContext, "baz"],\
|
||||||
|
// you can also provide a component, the value will be passed to a `value` prop\
|
||||||
|
[MyCustomProviderComponent, "hello-world"],\
|
||||||
|
// if you have a provider that doesn't accept a `value` prop, you can just pass a function\
|
||||||
|
BoundContextProvider,\
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<App />
|
||||||
|
</MultiProvider>;
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
> Components and values passed to `MultiProvider` will be evaluated only once, so make sure that the structure is static. If is isn't, please use nested provider components instead.
|
||||||
|
|
||||||
|
## [\#](https://primitives.solidjs.community/package/context/\#changelog) Changelog
|
||||||
|
|
||||||
|
See [CHANGELOG.md](https://github.com/solidjs-community/solid-primitives/blob/main/packages/context/CHANGELOG.md)
|
||||||
281
.firecrawl/solidjs-ui-libs.md
Normal file
281
.firecrawl/solidjs-ui-libs.md
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
As more developers are adopting [SolidJS](https://www.solidjs.com/?ref=yon.fun) for its fine-grained reactivity and impressive performance, finding the right UI library that is easy to use, flexible, and efficient has become even more important.
|
||||||
|
|
||||||
|
The right UI components can make a significant difference in how quickly and smoothly your project gets completed, ensuring both a great developer experience and user satisfaction.
|
||||||
|
|
||||||
|
According to [recent statistics](https://www.statista.com/statistics/1124699/worldwide-developer-survey-most-used-frameworks-web/?ref=yon.fun), SolidJS is used by 1.2% of developers worldwide (as of 2024), indicating that more developers are seeking powerful tools to improve their productivity.
|
||||||
|
|
||||||
|
SolidJS Usage Worldwide as of 2024
|
||||||
|
|
||||||
|
In this article, we'll talk about some of the [best UI libraries for SolidJS](https://yon.fun/solidjs-ui-libs/), ranked by how complex they are, their features, and how easy they are to use.
|
||||||
|
|
||||||
|
Let's explore what each library offers and how it can fit your development needs!
|
||||||
|
|
||||||
|
[Top 15 Best Lightweight CSS Frameworks (JS-Free)\\
|
||||||
|
\\
|
||||||
|
Find the best lightweight CSS frameworks for responsive, customizable, and JS-free solutions. Discover our top 15 picks, designed to simplify your design process and improve website performance.\\
|
||||||
|
\\
|
||||||
|
The Art of Dev.Ion Prodan\\
|
||||||
|
\\
|
||||||
|
](https://yon.fun/top-10-css-frameworks/)
|
||||||
|
|
||||||
|
## 1\. Kobalte
|
||||||
|
|
||||||
|
Kobalte Homepage
|
||||||
|
|
||||||
|
[Kobalte](https://kobalte.dev/docs/core/overview/introduction?ref=yon.fun) is a UI toolkit designed to build accessible web apps and design systems with SolidJS. It offers a collection of low-level components and primitives that provide the building blocks for creating a design system from scratch.
|
||||||
|
|
||||||
|
**Accessibility is at the forefront of Kobalte**, making it an ideal option for developers prioritizing inclusive design.
|
||||||
|
|
||||||
|
### Why Choose Kobalte?
|
||||||
|
|
||||||
|
_Kobalte is an excellent choice if accessibility is a priority for your project_. It offers fine-grained component control and a high level of flexibility, ideal for building truly custom user interfaces.
|
||||||
|
|
||||||
|
The unstyled approach allows developers to bring in their preferred styling solutions, whether it's vanilla CSS, Tailwind, or CSS-in-JS.
|
||||||
|
|
||||||
|
### Pros:
|
||||||
|
|
||||||
|
- Focuses on accessibility following [WAI-ARIA Authoring](https://www.w3.org/WAI/ARIA/apg/?ref=yon.fun) Practices.
|
||||||
|
- Composable components with granular access to component parts.
|
||||||
|
- Fully unstyled, allowing for complete customization.
|
||||||
|
|
||||||
|
### Cons:
|
||||||
|
|
||||||
|
- Requires more effort for styling since components are unstyled by default.
|
||||||
|
|
||||||
|
## 2\. SUID
|
||||||
|
|
||||||
|
SUID homepage
|
||||||
|
|
||||||
|
[SUID](https://suid.io/?ref=yon.fun) is a popular UI library built on [Material-UI (MUI)](https://mui.com/?ref=yon.fun) but made to work with SolidJS.
|
||||||
|
|
||||||
|
**It has over 50 components with full TypeScript support and works well with your SolidJS projects.**
|
||||||
|
|
||||||
|
SUID keeps the same style as MUI, which makes it great for developers who have used MUI before but want to work with SolidJS. It also comes with material design themes and is known for its flexibility and modern look.
|
||||||
|
|
||||||
|
### Why Choose SUID?
|
||||||
|
|
||||||
|
If you're used to React and MUI, this library will feel familiar. It provides extensive customization options, making it versatile for many use cases.
|
||||||
|
|
||||||
|
However, if you need a very lightweight solution, SUID might not be the best choice since it can be a bit heavy due to Material-UI features.
|
||||||
|
|
||||||
|
### Pros:
|
||||||
|
|
||||||
|
- Familiar API for developers transitioning from React and MUI.
|
||||||
|
- Over 50 components with TypeScript support.
|
||||||
|
- Flexible and modern material design themes.
|
||||||
|
|
||||||
|
### Cons:
|
||||||
|
|
||||||
|
- Inherits some of the complexity and overhead from Material-UI.
|
||||||
|
- Not the most lightweight solution available.
|
||||||
|
|
||||||
|
## 3\. Solid Bootstrap
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[Solid Bootstrap](https://solid-libs.github.io/solid-bootstrap/?ref=yon.fun) is a version of the popular Bootstrap library for SolidJS. It's great for developers who want to build quickly and without much fuss.
|
||||||
|
|
||||||
|
It has all the classic Bootstrap components like buttons, cards, and modals, and makes them work well with SolidJS.
|
||||||
|
|
||||||
|
### Why Choose Solid Bootstrap?
|
||||||
|
|
||||||
|
This library is ideal for those who prefer the classic Bootstrap look and want to build fast without worrying about complex customizations.
|
||||||
|
|
||||||
|
For example, Solid Bootstrap works well for building administrative dashboards where you need standard UI elements like tables, forms, and navigation bars quickly and easily.
|
||||||
|
|
||||||
|
### Pros:
|
||||||
|
|
||||||
|
- Easy to integrate with existing Bootstrap styles.
|
||||||
|
- Ideal for developers already familiar with Bootstrap.
|
||||||
|
- Great for quickly building admin dashboards.
|
||||||
|
|
||||||
|
### Cons:
|
||||||
|
|
||||||
|
- Limited flexibility in customizing beyond Bootstrap's standard components.
|
||||||
|
- May not be ideal for highly unique or modern UI designs.
|
||||||
|
|
||||||
|
## 4\. Flowbite Solid
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[Flowbite Solid](https://flowbite.com/docs/getting-started/solid-js/?ref=yon.fun) is a set of UI components built with Tailwind CSS and designed for SolidJS. It helps you create clean and responsive designs easily.
|
||||||
|
|
||||||
|
Flowbite components are very customizable and use Tailwind's utility classes, which means you can adjust things like spacing, colors, and fonts easily to make sure your design matches your brand.
|
||||||
|
|
||||||
|
### Why Choose Flowbite Solid?
|
||||||
|
|
||||||
|
If you are a fan of Tailwind CSS and prefer using utility classes to maintain flexibility in styling, Flowbite Solid is a solid option.
|
||||||
|
|
||||||
|
Its responsive and accessible components make it perfect for projects that need a consistent, visually appealing design.
|
||||||
|
|
||||||
|
### Pros:
|
||||||
|
|
||||||
|
- Fully compatible with Tailwind CSS, making it easy to customize.
|
||||||
|
- Responsive and accessible components.
|
||||||
|
- Great for building visually appealing, consistent designs.
|
||||||
|
|
||||||
|
### Cons:
|
||||||
|
|
||||||
|
- Requires familiarity with Tailwind CSS for effective use.
|
||||||
|
- Moderate complexity may not suit quick or simple projects.
|
||||||
|
|
||||||
|
[7 Best Free No-JS Tailwind CSS Component Libraries\\
|
||||||
|
\\
|
||||||
|
Discover the top 7 lightweight Tailwind CSS component libraries that require no JavaScript. These libraries offer customizable, responsive UI components to streamline your development process and create professional, consistent designs quickly and efficiently.\\
|
||||||
|
\\
|
||||||
|
The Art of Dev.Ion Prodan\\
|
||||||
|
\\
|
||||||
|
](https://yon.fun/top-tailwind-component-libs/)
|
||||||
|
|
||||||
|
## 5\. Ark UI
|
||||||
|
|
||||||
|
Ark UI Homepage
|
||||||
|
|
||||||
|
[Ark UI](https://ark-ui.com/?ref=yon.fun) is a headless UI library, which means it lets you create fully customizable components. It works with many JavaScript frameworks like SolidJS, React, and Vue.
|
||||||
|
|
||||||
|
Ark UI is focused on giving developers lots of flexibility without forcing any specific styles, so you can create unique and accessible components.
|
||||||
|
|
||||||
|
### Why Choose Ark UI?
|
||||||
|
|
||||||
|
If you want full control over how your components look and aim to create something truly unique, Ark UI is a powerful option.
|
||||||
|
|
||||||
|
However, because it is a headless library, it has a steeper learning curve and requires more setup and styling compared to other options.
|
||||||
|
|
||||||
|
### Pros:
|
||||||
|
|
||||||
|
- Full control over styling with headless components.
|
||||||
|
- Supports multiple frameworks, including SolidJS, React, and Vue.
|
||||||
|
- Highly customizable and accessible.
|
||||||
|
|
||||||
|
### Cons:
|
||||||
|
|
||||||
|
- Steeper learning curve compared to styled component libraries.
|
||||||
|
- Requires more effort to style and set up compared to simpler libraries.
|
||||||
|
|
||||||
|
## 6\. SolidUI
|
||||||
|
|
||||||
|
SolidUI Homepage
|
||||||
|
|
||||||
|
[SolidUI](https://www.solid-ui.com/?ref=yon.fun) gives you a set of nice-looking components that you can easily use in your SolidJS projects. It is easy to customize and is perfect for making quick prototypes or simple web interfaces without much effort.
|
||||||
|
|
||||||
|
SolidUI is also an unofficial port of [shadcn/ui](https://ui.shadcn.com/?ref=yon.fun) to SolidJS and is not affiliated with @shadcn.
|
||||||
|
|
||||||
|
When working with SolidJS and libraries like Solid-UI, you'll often deal with reactive components.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need to organize and snapshot these code pieces efficiently, I recommend [SnipsCo](https://snipsco.com/?ref=yon.fun) \- it's great for developers managing multiple UI snippets.
|
||||||
|
|
||||||
|
### Why Choose SolidUI?
|
||||||
|
|
||||||
|
If you want a simple and fast solution with ready-made components, SolidUI is an excellent option.
|
||||||
|
|
||||||
|
Compared to Solid Bootstrap, SolidUI is geared towards providing out-of-the-box components, helping you start building immediately without spending time on extra customization.
|
||||||
|
|
||||||
|
As it is an unofficial port of shadcn/ui, it offers a similar experience for developers already familiar with shadcn.
|
||||||
|
|
||||||
|
### Pros:
|
||||||
|
|
||||||
|
- Easy drag-and-drop integration.
|
||||||
|
- Customizable components that work well without much effort.
|
||||||
|
- Great for quick prototyping.
|
||||||
|
|
||||||
|
### Cons:
|
||||||
|
|
||||||
|
- Limited customization compared to headless or more complex libraries.
|
||||||
|
- Not ideal for larger, complex projects that need advanced functionality.
|
||||||
|
|
||||||
|
## 7\. shadcn-solid
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[shadcn-solid](https://shadcn-solid.com/?ref=yon.fun) is also an unofficial version of the popular [shadcn/ui library](https://ui.shadcn.com/?ref=yon.fun) made for SolidJS.
|
||||||
|
|
||||||
|
It has lots of customizable and accessible components that are easy to add to your SolidJS apps. The goal of this library is to give developers a smooth experience by providing consistent design and tools that are easy to use.
|
||||||
|
|
||||||
|
### Why Choose shadcn-solid?
|
||||||
|
|
||||||
|
If you like the design and usability of shadcn/ui and wish to adapt it for SolidJS, this library is ideal.
|
||||||
|
|
||||||
|
It works well for projects that need a consistent design across different platforms, providing accessible and easily customizable components.
|
||||||
|
|
||||||
|
### Pros:
|
||||||
|
|
||||||
|
- Based on the popular shadcn/ui, making it familiar to many developers.
|
||||||
|
- Components are accessible and easy to customize.
|
||||||
|
- Suitable for projects needing a consistent design across different platforms.
|
||||||
|
|
||||||
|
### Cons:
|
||||||
|
|
||||||
|
- Less feature-rich compared to some other medium or high-complexity libraries.
|
||||||
|
- Customization is easier, but can still be limited by the library's design choices.
|
||||||
|
|
||||||
|
## 8\. Corvu
|
||||||
|
|
||||||
|
Corvu Homepage
|
||||||
|
|
||||||
|
[Corvu](https://corvu.dev/?ref=yon.fun) is a collection of basic UI building blocks for SolidJS. Unlike other libraries, **Corvu does not come with pre-styled components**, which means you have full control over the look and feel of your app.
|
||||||
|
|
||||||
|
It's perfect if you want to create something truly custom and don't mind doing all the styling work yourself.
|
||||||
|
|
||||||
|
### Why Choose Corvu?
|
||||||
|
|
||||||
|
If you want to create your UI from scratch and have full control over every detail, Corvu is the perfect tool.
|
||||||
|
|
||||||
|
It's great for developers who are comfortable doing their styling and want to create a highly unique, custom UI.
|
||||||
|
|
||||||
|
### Pros:
|
||||||
|
|
||||||
|
- Provides maximum control by offering basic UI building blocks.
|
||||||
|
- Perfect for creating highly unique and custom UIs.
|
||||||
|
- Focused on accessibility and developer flexibility.
|
||||||
|
|
||||||
|
### Cons:
|
||||||
|
|
||||||
|
- Requires developers to handle all aspects of styling.
|
||||||
|
- Not suitable for those who need ready-to-use components.
|
||||||
|
|
||||||
|
## Comparison Table of SolidJS UI Libraries
|
||||||
|
|
||||||
|
| Library Name | Complexity | Key Features | Best Use Case |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Kobalte | Medium | Accessible, composable, unstyled | Building accessible and fully custom UIs |
|
||||||
|
| SUID | Medium | TypeScript support, Material-UI API | Transitioning from React to SolidJS |
|
||||||
|
| Solid Bootstrap | Low | Bootstrap components, easy integration | Quick dashboards and admin panels |
|
||||||
|
| Flowbite Solid | Medium | Tailwind CSS integration, responsive components | Projects using Tailwind CSS for styling |
|
||||||
|
| Ark UI | High | Headless components, full customization | Unique, highly customized UI projects |
|
||||||
|
| SolidUI | Low | Pre-designed components, easy setup | Rapid prototyping and simple web interfaces |
|
||||||
|
| shadcn-solid | Medium | Based on shadcn/ui, customizable components | Consistent design across platforms |
|
||||||
|
| Corvu | High | UI primitives, no default styles | Highly custom, unique styling from scratch |
|
||||||
|
|
||||||
|
_Table 1: Discover the best UI libraries for SolidJS, comparing complexity, key features, and the best use cases to help you choose the right tool for your project._
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Choosing the right UI library for your SolidJS project depends on what you need: _speed, customization, or something in between._
|
||||||
|
|
||||||
|
[Kobalte](https://yon.fun/solidjs-ui-libs/#1-kobalte) is ideal if accessibility and composability are crucial, [SUID](https://yon.fun/solidjs-ui-libs/#2-suid) is great if you're used to React, [Solid Bootstrap](https://yon.fun/solidjs-ui-libs/#3-solid-bootstrap) is perfect if you like Bootstrap, and [Flowbite Solid](https://yon.fun/solidjs-ui-libs/#4-flowbite-solid) is ideal if you love Tailwind CSS.
|
||||||
|
|
||||||
|
For maximum control, [**Ark UI**](https://yon.fun/solidjs-ui-libs/#5-ark-ui) **and** [**Corvu**](https://yon.fun/solidjs-ui-libs/#8-corvu) **are good choices because they let you design however you want.**
|
||||||
|
|
||||||
|
If you like shadcn/ui, [shadcn-solid](https://yon.fun/solidjs-ui-libs/#7-shadcn-solid), and [SolidUI](https://yon.fun/solidjs-ui-libs/#6-solidui) are a great version of SolidJS.
|
||||||
|
|
||||||
|
We hope this guide helps you pick the best UI library for your next SolidJS project!
|
||||||
|
|
||||||
|
## FAQs:
|
||||||
|
|
||||||
|
### Which SolidJS UI library is best for beginners?
|
||||||
|
|
||||||
|
[SolidUI](https://yon.fun/solidjs-ui-libs/#6-solidui) and [Solid Bootstrap](https://yon.fun/solidjs-ui-libs/#3-solid-bootstrap) are ideal for beginners because they are easy to use and don't require a lot of custom styling.
|
||||||
|
|
||||||
|
### Can I use Bootstrap with SolidJS?
|
||||||
|
|
||||||
|
Yes, you can use Solid Bootstrap, which is a version of the popular Bootstrap library adapted for SolidJS.
|
||||||
|
|
||||||
|
### What are the benefits of using Ark UI for SolidJS?
|
||||||
|
|
||||||
|
Ark UI allows for complete customization, making it suitable for projects where you need a unique look and want full control over component styling.
|
||||||
|
|
||||||
|
Feel free to check out our further reading on SolidJS best practices to take your development to the next level.
|
||||||
536
.firecrawl/stanza-compound-components.md
Normal file
536
.firecrawl/stanza-compound-components.md
Normal file
@ -0,0 +1,536 @@
|
|||||||
|
Compound components let a group of related components share implicit state through Context, giving consumers full control over layout and composition. Think of how `<select>` and `<option>` work together in HTML — neither makes sense alone, but together they form a flexible API where the browser handles state internally. This is the same idea, applied to React component libraries.
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
- 1Compound components share state implicitly through React Context — child components read from the parent provider without any prop drilling
|
||||||
|
- 2The dot notation API (\`Tabs.Tab\`, \`Tabs.Panel\`) groups sub-components under the parent, making the API discoverable and signaling that these components belong together
|
||||||
|
- 3This pattern gives consumers total control over the markup: they can reorder children, wrap them in custom layouts, or conditionally render specific parts without the component library needing to anticipate every case
|
||||||
|
- 4Always throw a descriptive error when a sub-component is used outside its parent provider — silent failures waste hours of debugging time
|
||||||
|
- 5Supporting both controlled and uncontrolled modes makes compound components drop-in for simple use cases and fully integrable in complex state management scenarios
|
||||||
|
- 6Every major headless UI library (Radix, Headless UI, Ark UI) uses compound components as their primary API pattern — this is not a niche technique, it is the industry standard for component library design
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### The problem: a props-heavy component that falls apart
|
||||||
|
|
||||||
|
tsxCopy
|
||||||
|
|
||||||
|
```
|
||||||
|
// This is what compound components replace.
|
||||||
|
// Every new requirement means another prop.
|
||||||
|
type AccordionProps = {
|
||||||
|
items: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
headerClassName?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
}[];
|
||||||
|
allowMultiple?: boolean;
|
||||||
|
defaultOpen?: string[];
|
||||||
|
onChange?: (openIds: string[]) => void;
|
||||||
|
renderHeader?: (item: Item, isOpen: boolean) => ReactNode;
|
||||||
|
renderContent?: (item: Item) => ReactNode;
|
||||||
|
headerAs?: ElementType;
|
||||||
|
animated?: boolean;
|
||||||
|
variant?: 'default' | 'bordered' | 'ghost';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 50 lines in, and the consumer still can't put
|
||||||
|
// a badge counter next to one specific header.
|
||||||
|
// This API does not scale.
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration-object APIs hit a wall fast. Every layout variation becomes another prop, and custom rendering requires render props bolted onto an already crowded interface. Compound components solve this by giving the consumer JSX instead of config.
|
||||||
|
|
||||||
|
### Tabs — the classic compound component
|
||||||
|
|
||||||
|
tsxCopy
|
||||||
|
|
||||||
|
```
|
||||||
|
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type TabsContext = {
|
||||||
|
activeTab: string;
|
||||||
|
setActiveTab: (id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TabsCtx = createContext<TabsContext | null>(null);
|
||||||
|
|
||||||
|
function useTabs() {
|
||||||
|
const ctx = useContext(TabsCtx);
|
||||||
|
if (!ctx) throw new Error('Tabs compound components must be rendered inside <Tabs>');
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
|
||||||
|
const [activeTab, setActiveTab] = useState(defaultTab);
|
||||||
|
return (
|
||||||
|
<TabsCtx.Provider value={{ activeTab, setActiveTab }}>
|
||||||
|
<div className="tabs">{children}</div>
|
||||||
|
</TabsCtx.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabList({ children }: { children: ReactNode }) {
|
||||||
|
return <div role="tablist" className="tab-list">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tab({ id, children }: { id: string; children: ReactNode }) {
|
||||||
|
const { activeTab, setActiveTab } = useTabs();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === id}
|
||||||
|
className={activeTab === id ? 'tab active' : 'tab'}
|
||||||
|
onClick={() => setActiveTab(id)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabPanel({ id, children }: { id: string; children: ReactNode }) {
|
||||||
|
const { activeTab } = useTabs();
|
||||||
|
if (activeTab !== id) return null;
|
||||||
|
return <div role="tabpanel">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tabs.List = TabList;
|
||||||
|
Tabs.Tab = Tab;
|
||||||
|
Tabs.Panel = TabPanel;
|
||||||
|
|
||||||
|
// Usage — the consumer controls everything
|
||||||
|
<Tabs defaultTab="code">
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab id="code">Code</Tabs.Tab>
|
||||||
|
<Tabs.Tab id="preview">Preview</Tabs.Tab>
|
||||||
|
<Tabs.Tab id="tests">Tests <Badge count={3} /></Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Panel id="code"><CodeEditor /></Tabs.Panel>
|
||||||
|
<Tabs.Panel id="preview"><LivePreview /></Tabs.Panel>
|
||||||
|
<Tabs.Panel id="tests"><TestRunner /></Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
```
|
||||||
|
|
||||||
|
The parent Tabs component owns the state and provides it through Context. Each sub-component reads only what it needs. Notice how the consumer can put a Badge inside a specific Tab without the component API knowing about badges at all. That flexibility is the entire point.
|
||||||
|
|
||||||
|
### Accordion with single and multi-expand modes
|
||||||
|
|
||||||
|
tsxCopy
|
||||||
|
|
||||||
|
```
|
||||||
|
type AccordionCtx = {
|
||||||
|
openItems: Set<string>;
|
||||||
|
toggle: (id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccordionCtx = createContext<AccordionCtx | null>(null);
|
||||||
|
|
||||||
|
function useAccordion() {
|
||||||
|
const ctx = useContext(AccordionCtx);
|
||||||
|
if (!ctx) throw new Error('<Accordion.*> must be used within <Accordion>');
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccordionProps = {
|
||||||
|
allowMultiple?: boolean;
|
||||||
|
defaultOpen?: string[];
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Accordion({ allowMultiple = false, defaultOpen = [], children }: AccordionProps) {
|
||||||
|
const [openItems, setOpenItems] = useState(new Set(defaultOpen));
|
||||||
|
|
||||||
|
const toggle = (id: string) => {
|
||||||
|
setOpenItems(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id);
|
||||||
|
} else {
|
||||||
|
if (!allowMultiple) next.clear();
|
||||||
|
next.add(id);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionCtx.Provider value={{ openItems, toggle }}>
|
||||||
|
<div className="accordion">{children}</div>
|
||||||
|
</AccordionCtx.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Item({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
|
return <div className={`accordion-item ${className ?? ''}`}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Trigger({ id, children }: { id: string; children: ReactNode }) {
|
||||||
|
const { openItems, toggle } = useAccordion();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-expanded={openItems.has(id)}
|
||||||
|
aria-controls={`panel-${id}`}
|
||||||
|
onClick={() => toggle(id)}
|
||||||
|
className="accordion-trigger"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronIcon rotated={openItems.has(id)} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Content({ id, children }: { id: string; children: ReactNode }) {
|
||||||
|
const { openItems } = useAccordion();
|
||||||
|
if (!openItems.has(id)) return null;
|
||||||
|
return <div id={`panel-${id}`} className="accordion-content">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Accordion.Item = Item;
|
||||||
|
Accordion.Trigger = Trigger;
|
||||||
|
Accordion.Content = Content;
|
||||||
|
|
||||||
|
// FAQ page
|
||||||
|
<Accordion allowMultiple defaultOpen={['q1']}>
|
||||||
|
<Accordion.Item>
|
||||||
|
<Accordion.Trigger id="q1">How does billing work?</Accordion.Trigger>
|
||||||
|
<Accordion.Content id="q1">
|
||||||
|
<p>You are billed monthly based on your plan tier.</p>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item>
|
||||||
|
<Accordion.Trigger id="q2">Can I cancel anytime?</Accordion.Trigger>
|
||||||
|
<Accordion.Content id="q2">
|
||||||
|
<p>Yes, there are no long-term contracts.</p>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
```
|
||||||
|
|
||||||
|
The Accordion manages which items are open via a Set. The allowMultiple flag determines whether opening one item closes the others. Sub-components only interact with the context they need: Trigger calls toggle, Content reads openItems. This separation means you can place Trigger and Content anywhere in the tree as long as they are inside an Accordion.
|
||||||
|
|
||||||
|
### Select/Dropdown with keyboard navigation
|
||||||
|
|
||||||
|
tsxCopy
|
||||||
|
|
||||||
|
```
|
||||||
|
type SelectCtx = {
|
||||||
|
isOpen: boolean;
|
||||||
|
selectedValue: string | null;
|
||||||
|
highlightedIndex: number;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
select: (value: string) => void;
|
||||||
|
setHighlightedIndex: (index: number) => void;
|
||||||
|
options: string[];
|
||||||
|
registerOption: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectCtx = createContext<SelectCtx | null>(null);
|
||||||
|
|
||||||
|
function useSelect() {
|
||||||
|
const ctx = useContext(SelectCtx);
|
||||||
|
if (!ctx) throw new Error('Select.* must be used within <Select>');
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Select({ children, onChange }: { children: ReactNode; onChange?: (v: string) => void }) {
|
||||||
|
const [isOpen, setOpen] = useState(false);
|
||||||
|
const [selectedValue, setSelectedValue] = useState<string | null>(null);
|
||||||
|
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||||
|
const [options, setOptions] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const registerOption = (value: string) => {
|
||||||
|
setOptions(prev => prev.includes(value) ? prev : [...prev, value]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const select = (value: string) => {
|
||||||
|
setSelectedValue(value);
|
||||||
|
setOpen(false);
|
||||||
|
onChange?.(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'ArrowDown') setHighlightedIndex(i => Math.min(i + 1, options.length - 1));
|
||||||
|
if (e.key === 'ArrowUp') setHighlightedIndex(i => Math.max(i - 1, 0));
|
||||||
|
if (e.key === 'Enter' && isOpen) select(options[highlightedIndex]);
|
||||||
|
if (e.key === 'Escape') setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectCtx.Provider value={{
|
||||||
|
isOpen, selectedValue, highlightedIndex,
|
||||||
|
setOpen, select, setHighlightedIndex,
|
||||||
|
options, registerOption
|
||||||
|
}}>
|
||||||
|
<div className="select" onKeyDown={handleKeyDown} tabIndex={0}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</SelectCtx.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Trigger({ children, placeholder }: { children?: ReactNode; placeholder?: string }) {
|
||||||
|
const { isOpen, setOpen, selectedValue } = useSelect();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="select-trigger"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
onClick={() => setOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
{selectedValue ?? placeholder ?? 'Select...'}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Options({ children }: { children: ReactNode }) {
|
||||||
|
const { isOpen } = useSelect();
|
||||||
|
if (!isOpen) return null;
|
||||||
|
return <ul role="listbox" className="select-options">{children}</ul>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Option({ value, children }: { value: string; children: ReactNode }) {
|
||||||
|
const { select, selectedValue, registerOption } = useSelect();
|
||||||
|
useEffect(() => { registerOption(value); }, [value]);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
role="option"
|
||||||
|
aria-selected={selectedValue === value}
|
||||||
|
onClick={() => select(value)}
|
||||||
|
className={selectedValue === value ? 'option selected' : 'option'}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Select.Trigger = Trigger;
|
||||||
|
Select.Options = Options;
|
||||||
|
Select.Option = Option;
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Select onChange={handleLanguageChange}>
|
||||||
|
<Select.Trigger placeholder="Pick a language" />
|
||||||
|
<Select.Options>
|
||||||
|
<Select.Option value="typescript">TypeScript</Select.Option>
|
||||||
|
<Select.Option value="rust">Rust</Select.Option>
|
||||||
|
<Select.Option value="go">Go</Select.Option>
|
||||||
|
</Select.Options>
|
||||||
|
</Select>
|
||||||
|
```
|
||||||
|
|
||||||
|
A Select component demonstrates a more complex compound pattern. The parent manages open state, selection, keyboard navigation, and an option registry. Each Option registers itself on mount so the parent knows about all available options for keyboard navigation. This is a simplified version of what libraries like Radix UI and Headless UI ship.
|
||||||
|
|
||||||
|
### Modal with Header, Body, Footer slots
|
||||||
|
|
||||||
|
tsxCopy
|
||||||
|
|
||||||
|
```
|
||||||
|
import { createContext, useContext, ReactNode, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
type ModalCtx = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalCtx = createContext<ModalCtx | null>(null);
|
||||||
|
|
||||||
|
function useModal() {
|
||||||
|
const ctx = useContext(ModalCtx);
|
||||||
|
if (!ctx) throw new Error('Modal.* must be used within <Modal>');
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModalProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Modal({ isOpen, onClose, children }: ModalProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleEscape);
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<ModalCtx.Provider value={{ onClose }}>
|
||||||
|
<div className="modal-overlay" onClick={onClose}>
|
||||||
|
<div
|
||||||
|
className="modal-content"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalCtx.Provider>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header({ children }: { children: ReactNode }) {
|
||||||
|
const { onClose } = useModal();
|
||||||
|
return (
|
||||||
|
<div className="modal-header">
|
||||||
|
{children}
|
||||||
|
<button onClick={onClose} aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Body({ children }: { children: ReactNode }) {
|
||||||
|
return <div className="modal-body">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Footer({ children }: { children: ReactNode }) {
|
||||||
|
return <div className="modal-footer">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.Header = Header;
|
||||||
|
Modal.Body = Body;
|
||||||
|
Modal.Footer = Footer;
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Modal isOpen={showConfirm} onClose={() => setShowConfirm(false)}>
|
||||||
|
<Modal.Header><h2>Delete project?</h2></Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p>This will permanently delete <strong>{project.name}</strong> and all its data.</p>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="ghost" onClick={() => setShowConfirm(false)}>Cancel</Button>
|
||||||
|
<Button variant="danger" onClick={handleDelete}>Delete</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
The Modal shares onClose through Context so the Header can render a close button without the consumer wiring it up. The Footer is optional — if you only need a confirmation message, skip it entirely. Portals ensure the modal escapes parent overflow and z-index stacking. This pattern shows compound components working beyond just state sharing — they also provide structural conventions.
|
||||||
|
|
||||||
|
### Controlled mode — letting parents own the state
|
||||||
|
|
||||||
|
tsxCopy
|
||||||
|
|
||||||
|
```
|
||||||
|
type TabsProps = {
|
||||||
|
// Uncontrolled: component owns state
|
||||||
|
defaultTab?: string;
|
||||||
|
// Controlled: parent owns state
|
||||||
|
activeTab?: string;
|
||||||
|
onTabChange?: (id: string) => void;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Tabs({ defaultTab, activeTab: controlledTab, onTabChange, children }: TabsProps) {
|
||||||
|
const [internalTab, setInternalTab] = useState(defaultTab ?? '');
|
||||||
|
|
||||||
|
const isControlled = controlledTab !== undefined;
|
||||||
|
const activeTab = isControlled ? controlledTab : internalTab;
|
||||||
|
|
||||||
|
const setActiveTab = (id: string) => {
|
||||||
|
if (!isControlled) setInternalTab(id);
|
||||||
|
onTabChange?.(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsCtx.Provider value={{ activeTab, setActiveTab }}>
|
||||||
|
<div className="tabs">{children}</div>
|
||||||
|
</TabsCtx.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncontrolled — fire and forget
|
||||||
|
<Tabs defaultTab="overview">
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab id="overview">Overview</Tabs.Tab>
|
||||||
|
<Tabs.Tab id="api">API</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Panel id="overview">...</Tabs.Panel>
|
||||||
|
<Tabs.Panel id="api">...</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
// Controlled — parent syncs with URL params
|
||||||
|
const [tab, setTab] = useState(searchParams.get('tab') ?? 'overview');
|
||||||
|
|
||||||
|
<Tabs activeTab={tab} onTabChange={(id) => {
|
||||||
|
setTab(id);
|
||||||
|
setSearchParams({ tab: id });
|
||||||
|
}}>
|
||||||
|
{/* same children */}
|
||||||
|
</Tabs>
|
||||||
|
```
|
||||||
|
|
||||||
|
Supporting both modes is essential for real-world compound components. Uncontrolled mode is convenient for static UIs. Controlled mode is necessary when the active state needs to sync with URL parameters, form state, or other components. The pattern checks whether a controlled value is provided and delegates accordingly.
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
Mistake:
|
||||||
|
|
||||||
|
Using \`React.Children.map\` and \`cloneElement\` to inject props into children instead of using Context
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
Context works regardless of nesting depth and does not break when consumers wrap sub-components in divs, fragments, or custom wrappers. cloneElement only works on direct children and silently fails when the tree structure changes. Context is the correct primitive for compound components.
|
||||||
|
|
||||||
|
Mistake:
|
||||||
|
|
||||||
|
Failing to throw an error when a sub-component is used outside its parent provider — returning null or using a default value instead
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
Always throw a descriptive error in the custom hook: \`throw new Error('Tabs.Tab must be used within <Tabs>')\`. Silent failures lead to hours of debugging. The developer needs to know immediately that they placed a component in the wrong part of the tree.
|
||||||
|
|
||||||
|
Mistake:
|
||||||
|
|
||||||
|
Putting too much into the Context value — exposing internal state, dispatch functions, and derived values that sub-components never use
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
Keep the context value minimal. If some sub-components need data that others do not, consider splitting into two contexts (one for state, one for dispatch) or providing only what the public sub-components actually read. A bloated context causes unnecessary re-renders across all consumers.
|
||||||
|
|
||||||
|
Mistake:
|
||||||
|
|
||||||
|
Creating a new context value object on every render, which triggers re-renders in all consumers even when the actual state has not changed
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
Memoize the context value with \`useMemo\`: \`const value = useMemo(() => ({ activeTab, setActiveTab }), \[activeTab\])\`. This ensures consumers only re-render when the state they depend on actually changes.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Use the dot notation pattern (\`Tabs.Tab\`, \`Tabs.Panel\`) to attach sub-components to the parent. This makes the API self-documenting — autocomplete shows all available sub-components when you type \`Tabs.\`
|
||||||
|
- Support both controlled and uncontrolled modes. Accept \`defaultValue\` for uncontrolled usage and \`value\` + \`onChange\` for controlled usage. Check if the controlled prop is defined to determine which mode to use.
|
||||||
|
- Memoize the Context value with \`useMemo\` to prevent unnecessary re-renders. This matters especially when the parent component has props or state unrelated to the compound component's shared state.
|
||||||
|
- Write a custom hook (e.g., \`useTabs\`, \`useAccordion\`) that throws on null context instead of using \`useContext\` directly in sub-components. This centralizes error handling and gives you a single place to add logging or validation.
|
||||||
|
- Keep sub-components focused on one responsibility. The Trigger handles click events, the Content handles visibility, the Item provides grouping. Do not merge responsibilities — it defeats the composability that makes this pattern valuable.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Compound components are a group of related React components that share implicit state through Context, giving consumers full control over markup and layout. The parent component provides state via a Context Provider, and sub-components consume it through a custom hook. This pattern replaces sprawling props-based APIs with a composable, declarative interface. Attach sub-components using dot notation for discoverability, throw clear errors when components are used outside their parent, memoize the context value, and support both controlled and uncontrolled modes. Every serious React component library — Radix, Headless UI, Ark UI — builds on this pattern.
|
||||||
|
|
||||||
|
## Go deeper with Stanza
|
||||||
|
|
||||||
|
Practice react compound components with interactive lessons and challenges.
|
||||||
|
|
||||||
|
[React Components & Patterns](https://www.stanza.dev/courses/react-components-patterns) [React 19 & Patterns](https://www.stanza.dev/courses/react-modern-patterns)
|
||||||
|
|
||||||
|
## Related Concepts
|
||||||
|
|
||||||
|
[React Context Api](https://www.stanza.dev/concepts/react-context-api "React Context Api") [React Custom Hooks](https://www.stanza.dev/concepts/react-custom-hooks "React Custom Hooks") [React Usestate](https://www.stanza.dev/concepts/react-usestate "React Usestate") [React Error Boundaries](https://www.stanza.dev/concepts/react-error-boundaries "React Error Boundaries")
|
||||||
|
|
||||||
|
## Related Cheatsheets
|
||||||
|
|
||||||
|
[React Hooks Cheatsheet](https://www.stanza.dev/cheatsheet/react-hooks "React Hooks Cheatsheet")
|
||||||
1027
.firecrawl/state-of-react-2025.md
Normal file
1027
.firecrawl/state-of-react-2025.md
Normal file
File diff suppressed because it is too large
Load Diff
239
.firecrawl/ui-lab-ai-integration.md
Normal file
239
.firecrawl/ui-lab-ai-integration.md
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
#### AI integration
|
||||||
|
|
||||||
|
Specifications and patterns for integrating AI capabilities with UI Lab components.
|
||||||
|
|
||||||
|
## Accessing LLMs.txt
|
||||||
|
|
||||||
|
After installing UI Lab, you can reference the documentation in multiple ways:
|
||||||
|
|
||||||
|
### Method 1: Local file
|
||||||
|
|
||||||
|
The LLMs.txt file is included in the package. Reference it from your node\_modules:
|
||||||
|
|
||||||
|
```
|
||||||
|
node_modules/@ui-lab/core/LLMs.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: CLI
|
||||||
|
|
||||||
|
Print LLMs.txt content to the terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx ui-lab llms
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 3: Web documentation
|
||||||
|
|
||||||
|
Visit the UI Lab website for interactive component reference documentation.
|
||||||
|
|
||||||
|
## Using with AI tools
|
||||||
|
|
||||||
|
### ChatGPT / Claude / Copilot
|
||||||
|
|
||||||
|
Provide the LLMs.txt content when asking AI tools to generate components. Copy and paste the documentation at the start of your conversation:
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a React/TypeScript developer. Use UI Lab components for the UI layer.
|
||||||
|
|
||||||
|
Here is the complete component documentation:
|
||||||
|
|
||||||
|
[Copy LLMs.txt content here]
|
||||||
|
|
||||||
|
Now, build a user profile card that shows:
|
||||||
|
- User avatar
|
||||||
|
- User name and email
|
||||||
|
- Edit and delete buttons
|
||||||
|
- Responsive design
|
||||||
|
```
|
||||||
|
|
||||||
|
The AI will generate code using the documented components and patterns, respecting the design system constraints.
|
||||||
|
|
||||||
|
### IDE Extensions
|
||||||
|
|
||||||
|
Use GitHub Copilot or Cursor with the documentation. Add a comment with instructions:
|
||||||
|
|
||||||
|
```
|
||||||
|
// Use UI Lab components
|
||||||
|
// Button variants: primary, secondary, tertiary, destructive
|
||||||
|
// Card has Header, Title, Description, Content, Footer slots
|
||||||
|
// Build a login form with email, password, and submit button
|
||||||
|
|
||||||
|
export default function LoginForm() {
|
||||||
|
// IDE suggests code using UI Lab components
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example prompts
|
||||||
|
|
||||||
|
### Simple component
|
||||||
|
|
||||||
|
Start simple. Provide clear requirements and let the AI handle implementation:
|
||||||
|
|
||||||
|
```
|
||||||
|
Using UI Lab components, build a settings card with:
|
||||||
|
- Title: "Display Settings"
|
||||||
|
- Toggle for dark mode
|
||||||
|
- Dropdown for language selection
|
||||||
|
- Save button at the bottom
|
||||||
|
|
||||||
|
Make it accessible with proper labels and ARIA attributes.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex feature
|
||||||
|
|
||||||
|
For larger features, break down requirements and explain data structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
Build a product list component using UI Lab. Requirements:
|
||||||
|
|
||||||
|
Data:
|
||||||
|
- products: { id, name, price, category, inStock }[]
|
||||||
|
- isLoading: boolean
|
||||||
|
- error: string | null
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Display products in a responsive grid
|
||||||
|
- Show product card with name, price, category badge
|
||||||
|
- Disable purchase button if out of stock
|
||||||
|
- Show loading state with skeleton cards
|
||||||
|
- Display error message if loading fails
|
||||||
|
- Include pagination (10 items per page)
|
||||||
|
|
||||||
|
Use Tailwind classes for layout, UI Lab for components.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Style and behavior specifics
|
||||||
|
|
||||||
|
Be specific about behavior and styling to get better results:
|
||||||
|
|
||||||
|
```
|
||||||
|
Build a notification component using UI Lab:
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Type: success, error, warning, info
|
||||||
|
- Auto-dismiss after 5 seconds
|
||||||
|
- Allow manual close
|
||||||
|
- Stack multiple notifications vertically
|
||||||
|
- Show icon based on type
|
||||||
|
- Use proper semantic colors (success-500, destructive-500, etc)
|
||||||
|
- Animate in/out smoothly
|
||||||
|
- Position fixed at top-right
|
||||||
|
- Respond to keyboard (Escape to close)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best practices for AI code generation
|
||||||
|
|
||||||
|
### 1\. Provide context
|
||||||
|
|
||||||
|
Give the AI information about your project structure, state management (React hooks, Redux, etc), and any existing patterns. This helps generate code that fits your codebase.
|
||||||
|
|
||||||
|
### 2\. Include LLMs.txt early
|
||||||
|
|
||||||
|
Always provide the LLMs.txt documentation in the first message or system prompt. This prevents the AI from inventing components or props that don't exist.
|
||||||
|
|
||||||
|
### 3\. Review generated code
|
||||||
|
|
||||||
|
Even with AI guidance, review generated code for accessibility, performance, and correctness. Check for proper ARIA attributes, keyboard navigation, and semantic HTML.
|
||||||
|
|
||||||
|
### 4\. Test accessibility
|
||||||
|
|
||||||
|
Run accessibility checks on generated code. Use tools like axe DevTools, Lighthouse, or WebAIM to ensure components are truly accessible.
|
||||||
|
|
||||||
|
### 5\. Iterate with feedback
|
||||||
|
|
||||||
|
Provide feedback to the AI when code doesn't meet requirements. Iterate by pointing out issues and asking for adjustments rather than starting over.
|
||||||
|
|
||||||
|
### 6\. Don't bypass documentation
|
||||||
|
|
||||||
|
If the AI generates props or components that seem wrong, check the LLMs.txt documentation. The AI's understanding is only as good as the documentation provided.
|
||||||
|
|
||||||
|
## Understanding LLMs.txt structure
|
||||||
|
|
||||||
|
LLMs.txt is organized by section, making it easy to find what you need. A typical entry looks like:
|
||||||
|
|
||||||
|
````
|
||||||
|
## Button Component
|
||||||
|
|
||||||
|
### Props
|
||||||
|
- variant: 'primary' | 'secondary' | 'tertiary' | 'destructive'
|
||||||
|
- primary: High emphasis, use for main actions
|
||||||
|
- secondary: Medium emphasis, use for secondary actions
|
||||||
|
- tertiary: Low emphasis, for less important actions
|
||||||
|
- destructive: Dangerous actions, clearly marked
|
||||||
|
|
||||||
|
- size: 'sm' | 'md' | 'lg'
|
||||||
|
- disabled: boolean
|
||||||
|
- loading: boolean - shows loading spinner, disables interaction
|
||||||
|
- type: 'button' | 'submit' | 'reset'
|
||||||
|
- className: string - merge with defaults using clsx
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```tsx
|
||||||
|
<Button variant="primary">Submit</Button>
|
||||||
|
<Button variant="destructive" size="lg">Delete</Button>
|
||||||
|
<Button disabled>Disabled</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
- Uses semantic <button> element
|
||||||
|
- Keyboard accessible (Enter, Space to activate)
|
||||||
|
- aria-busy set when loading
|
||||||
|
- aria-disabled set when disabled
|
||||||
|
````
|
||||||
|
|
||||||
|
Each component entry includes purpose, props with descriptions, examples, accessibility notes, and integration guidelines.
|
||||||
|
|
||||||
|
## Advanced scenarios
|
||||||
|
|
||||||
|
### Custom hooks with components
|
||||||
|
|
||||||
|
Ask the AI to generate custom hooks alongside components:
|
||||||
|
|
||||||
|
```
|
||||||
|
Create a useFormValidation hook and a SignupForm component using UI Lab.
|
||||||
|
|
||||||
|
Hook should:
|
||||||
|
- Track form values and validation state
|
||||||
|
- Support custom validation rules
|
||||||
|
- Provide error messages per field
|
||||||
|
|
||||||
|
Form should:
|
||||||
|
- Use the hook for state management
|
||||||
|
- Display validation errors below inputs
|
||||||
|
- Disable submit button if form is invalid
|
||||||
|
- Show loading state while submitting
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integrating with existing code
|
||||||
|
|
||||||
|
Share your existing code structure and ask for UI Lab components that fit:
|
||||||
|
|
||||||
|
```
|
||||||
|
I have this component:
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
rating: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
|
||||||
|
Refactor the rendering to use UI Lab Card, Badge, and Button
|
||||||
|
components. Keep the same data structure and logic.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
[**Getting started** \\
|
||||||
|
Learn how to use components in your project manually.](https://www.ui-lab.app/docs/getting-started) [**Accessibility** \\
|
||||||
|
Ensure AI-generated code meets a11y standards.](https://www.ui-lab.app/docs/accessibility) [**Best practices** \\
|
||||||
|
Learn patterns for effective component usage.](https://www.ui-lab.app/docs/best-practices) [**Customization** \\
|
||||||
|
Extend components with custom variants.](https://www.ui-lab.app/docs/customization)
|
||||||
|
|
||||||
|
Open Page
|
||||||
|
|
||||||
|
[Open in GitHub](https://github.com/kyza0d/ui-lab.app/blob/master/apps/site/content/docs/ai-integration.mdx) [Open in Claude](https://claude.ai/new?q=Read%20this%20and%20help%20me%20understand%20it%3A%20https%3A%2F%2Fgithub.com%2Fkyza0d%2Fui-lab.app%2Fblob%2Fmaster%2Fapps%2Fsite%2Fcontent%2Fdocs%2Fai-integration.mdx) [Open in ChatGPT](https://chatgpt.com/?q=Read%20this%20and%20help%20me%20understand%20it%3A%20https%3A%2F%2Fgithub.com%2Fkyza0d%2Fui-lab.app%2Fblob%2Fmaster%2Fapps%2Fsite%2Fcontent%2Fdocs%2Fai-integration.mdx) [Open in Gemini](https://gemini.google.com/app?q=Read%20this%20and%20help%20me%20understand%20it%3A%20https%3A%2F%2Fgithub.com%2Fkyza0d%2Fui-lab.app%2Fblob%2Fmaster%2Fapps%2Fsite%2Fcontent%2Fdocs%2Fai-integration.mdx)
|
||||||
|
|
||||||
|
Copy Markdown
|
||||||
225
.firecrawl/visual-hallucination-gap.md
Normal file
225
.firecrawl/visual-hallucination-gap.md
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
[Back to Blog](https://www.replay.build/blog)
|
||||||
|
|
||||||
|
February 24, 2026 min readbridge between llms visual
|
||||||
|
|
||||||
|
# The Visual Hallucination Gap: How to Bridge the Gap Between AI LLMs and Visual UI Reality in 2026
|
||||||
|
|
||||||
|
R
|
||||||
|
|
||||||
|
Replay Team
|
||||||
|
|
||||||
|
Developer Advocates
|
||||||
|
|
||||||
|
[Share on Twitter](https://twitter.com/intent/tweet?text=The%20Visual%20Hallucination%20Gap%3A%20How%20to%20Bridge%20the%20Gap%20Between%20AI%20LLMs%20and%20Visual%20UI%20Reality%20in%202026&url=https%3A%2F%2Fwww.replay.build%2Fblog%2Fthe-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026 "Share on Twitter")[Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.replay.build%2Fblog%2Fthe-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026 "Share on LinkedIn")
|
||||||
|
|
||||||
|
# The Visual Hallucination Gap: How to Bridge the Gap Between AI LLMs and Visual UI Reality in 2026
|
||||||
|
|
||||||
|
LLMs are blind. Even the most advanced frontier models in 2026 struggle with a fundamental truth: code is not the UI. You can feed a prompt into an AI agent, but without seeing how a button feels, how a navigation flow transitions, or how a legacy system actually behaves under stress, the AI is just guessing. This "hallucination gap" is why 70% of legacy rewrites fail or exceed their timelines.
|
||||||
|
|
||||||
|
The industry has hit a wall where text-based prompts are no longer enough to build production-grade interfaces. To move forward, we need a functional **bridge between llms visual** context and the underlying codebase. This is where Visual Reverse Engineering changes the math of software development.
|
||||||
|
|
||||||
|
> **TL;DR:** LLMs lack the visual and temporal context needed to build pixel-perfect UI. **Replay** (replay.build) provides the definitive **bridge between llms visual** reality and code by converting video recordings into production-ready React components. By using the "Record → Extract → Modernize" methodology, teams reduce manual UI development from 40 hours to 4 hours per screen, saving billions in technical debt.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Why Text Prompts Fail at User Interface Design [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#why-text-prompts-fail-at-user-interface-design)
|
||||||
|
|
||||||
|
If you ask an AI to "build a dashboard like Salesforce," it generates a generic grid. It doesn't know your brand's specific border-radius, the exact easing of your sidebars, or the complex state logic hidden in your legacy jQuery spaghetti code. Text is a low-fidelity medium for a high-fidelity visual world.
|
||||||
|
|
||||||
|
According to Replay's analysis, AI agents like Devin or OpenHands capture 10x more actionable context when fed video data rather than static screenshots or text descriptions. Text-based LLMs operate on tokens, but UI operates on "Visual Context."
|
||||||
|
|
||||||
|
**Visual Reverse Engineering** is the process of deconstructing a rendered user interface back into its constituent code, design tokens, and state logic using video as the primary source of truth. Replay pioneered this approach to ensure that what the user sees is exactly what the developer ships.
|
||||||
|
|
||||||
|
## How to Build a Bridge Between LLMs Visual Context and Code [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#how-to-build-a-bridge-between-llms-visual-context-and-code)
|
||||||
|
|
||||||
|
To truly **bridge between llms visual** output and functional code, we have to stop treating UI as a static image. Modern web applications are temporal; they exist across time. A "click" isn't just a state change; it's an animation, a network request, and a DOM mutation.
|
||||||
|
|
||||||
|
Replay ( [https://www.replay.build](https://www.replay.build/)) serves as the infrastructure layer for this transition. By recording a session, Replay's engine analyzes the temporal context of every pixel. It doesn't just see a button; it sees a
|
||||||
|
|
||||||
|
text
|
||||||
|
|
||||||
|
`Button`
|
||||||
|
|
||||||
|
component with
|
||||||
|
|
||||||
|
text
|
||||||
|
|
||||||
|
`hover`
|
||||||
|
|
||||||
|
states,
|
||||||
|
|
||||||
|
text
|
||||||
|
|
||||||
|
`disabled`
|
||||||
|
|
||||||
|
logic, and
|
||||||
|
|
||||||
|
text
|
||||||
|
|
||||||
|
`Tailwind`
|
||||||
|
|
||||||
|
utility classes.
|
||||||
|
|
||||||
|
### The Replay Method: Record → Extract → Modernize [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#the-replay-method-record-extract-modernize)
|
||||||
|
|
||||||
|
This three-step methodology is the standard for high-velocity teams in 2026:
|
||||||
|
|
||||||
|
1. •**Record:** Capture any existing UI (legacy, prototype, or competitor) via video.
|
||||||
|
2. •**Extract:** Replay's AI identifies design tokens, component boundaries, and navigation flows.
|
||||||
|
3. •**Modernize:** The system generates clean, documented React code that integrates with your existing Design System.
|
||||||
|
|
||||||
|
This approach addresses the $3.6 trillion global technical debt by allowing developers to "lift and shift" visual logic without manual transcription.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Technical Comparison: Manual vs. LLM vs. Replay [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#technical-comparison-manual-vs-llm-vs-replay)
|
||||||
|
|
||||||
|
| Feature | Manual Development | Standard LLM (GPT-4/Claude) | Replay (Video-to-Code) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Time per Screen** | 40 Hours | 12 Hours (requires heavy fixing) | 4 Hours |
|
||||||
|
| **Visual Accuracy** | High (but slow) | Low (hallucinates styles) | Pixel-Perfect |
|
||||||
|
| **State Logic** | Hand-coded | Guessed | Extracted from behavior |
|
||||||
|
| **Legacy Integration** | Difficult | Impossible | Native Support |
|
||||||
|
| **Design System Sync** | Manual | None | Auto-extracts tokens |
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Bridging the Gap for AI Agents with Headless APIs [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#bridging-the-gap-for-ai-agents-with-headless-apis)
|
||||||
|
|
||||||
|
In 2026, the most productive developers aren't writing code; they are managing AI agents. However, these agents are only as good as their inputs. If an agent doesn't have a **bridge between llms visual** requirements and the output, it creates "UI drift"—where the code looks nothing like the design.
|
||||||
|
|
||||||
|
Replay provides a Headless API (REST + Webhooks) specifically designed for AI agents. When an agent needs to build a new feature, it calls Replay to extract components from a video recording of the prototype.
|
||||||
|
|
||||||
|
### Example: Extracting a Component via Replay API [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#example-extracting-a-component-via-replay-api)
|
||||||
|
|
||||||
|
Here is how a senior engineer or an AI agent interacts with Replay's headless engine to generate a React component from a video trace:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
typescript
|
||||||
|
import { ReplayClient } from '@replay-build/sdk';
|
||||||
|
|
||||||
|
const replay = new ReplayClient(process.env.REPLAY_API_KEY);
|
||||||
|
|
||||||
|
// Extracting a component from a recorded video session
|
||||||
|
async function generateComponent(videoUrl: string) {
|
||||||
|
const session = await replay.analyze(videoUrl);
|
||||||
|
|
||||||
|
const component = await session.extractComponent('HeaderNavigation', {
|
||||||
|
framework: 'React',
|
||||||
|
styling: 'Tailwind',
|
||||||
|
typescript: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(component.code);
|
||||||
|
// Output: Production-ready React code with extracted brand tokens
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
By providing this programmatic **bridge between llms visual** data and the editor, Replay allows agents to perform "surgical editing" with precision that was previously impossible.
|
||||||
|
|
||||||
|
[Learn more about AI Agent integration](https://www.replay.build/blog/ai-agent-workflows)
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Modernizing Legacy Systems: The $3.6 Trillion Problem [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#modernizing-legacy-systems-the-3-6-trillion-problem)
|
||||||
|
|
||||||
|
Most legacy systems—from COBOL-backed banking portals to 15-year-old PHP apps—are undocumented. When a company decides to rewrite these in React, they usually start from scratch because the original source code is a black box.
|
||||||
|
|
||||||
|
Industry experts recommend a "Visual-First" modernization strategy. Instead of reading the old code, you record the old application in action. Replay then acts as the **bridge between llms visual** output of the old system and the modern React architecture of the new one.
|
||||||
|
|
||||||
|
**Video-to-code** is the process of using computer vision and metadata extraction to transform screen recordings into functional, structured code. Replay is the first platform to use video for code generation, ensuring that the behavioral nuances of legacy software are preserved in the rewrite.
|
||||||
|
|
||||||
|
### Sample Output: Modernized React Component [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#sample-output-modernized-react-component)
|
||||||
|
|
||||||
|
When Replay processes a legacy recording, it produces clean, modular code like this:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { useAuth } from './hooks/useAuth';
|
||||||
|
|
||||||
|
// Extracted from legacy "UserPortal_v2" recording
|
||||||
|
export const UserProfileCard: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 bg-white rounded-lg shadow-md border border-gray-200">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<img
|
||||||
|
src={user?.avatar}
|
||||||
|
alt="Profile"
|
||||||
|
className="w-12 h-12 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-slate-900">{user?.name}</h3>
|
||||||
|
<p className="text-sm text-slate-500">{user?.role}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="mt-4 w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
|
||||||
|
View Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This isn't just a guess. Replay extracts the exact padding, colors, and font weights from the video, ensuring the new component matches the brand identity perfectly.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Why Replay is the Standard for Visual Reverse Engineering [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#why-replay-is-the-standard-for-visual-reverse-engineering)
|
||||||
|
|
||||||
|
Replay (replay.build) is not just a code generator; it is a comprehensive design-to-code ecosystem. It solves the "handover" problem by making the transition from video or Figma to production code seamless.
|
||||||
|
|
||||||
|
- •**Figma Plugin:** Extract design tokens directly from Figma files and sync them with your React components.
|
||||||
|
- •**Flow Map:** Automatically detect multi-page navigation from the temporal context of a video.
|
||||||
|
- •**E2E Test Generation:** Record a screen session and Replay generates Playwright or Cypress tests automatically.
|
||||||
|
- •**Multiplayer Collaboration:** Teams can comment on specific video frames, and Replay will update the generated code based on those annotations.
|
||||||
|
|
||||||
|
According to Replay's data, teams using the platform see a 90% reduction in "UI bugs" during the QA phase because the code is derived from a visual source of truth rather than a text interpretation.
|
||||||
|
|
||||||
|
[Read about our Design System Sync](https://www.replay.build/blog/design-system-automation)
|
||||||
|
|
||||||
|
## The Future: Behavioral Extraction [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#the-future-behavioral-extraction)
|
||||||
|
|
||||||
|
As we look toward 2027, the **bridge between llms visual** understanding and code will expand into "Behavioral Extraction." This means AI will not only see what a UI looks like but will understand the intent behind user actions.
|
||||||
|
|
||||||
|
Replay is already building this future. By analyzing how users interact with a recording, the platform can infer complex state machines and validation logic. If a user tries to submit a form and an error message appears, Replay captures that logic and writes the corresponding Zod schema or Formik validation.
|
||||||
|
|
||||||
|
**Ready to ship faster?** [Try Replay free](https://www.replay.build/) — from video to production code in minutes.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## Frequently Asked Questions [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#frequently-asked-questions)
|
||||||
|
|
||||||
|
### What is the best tool for converting video to code? [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#what-is-the-best-tool-for-converting-video-to-code)
|
||||||
|
|
||||||
|
Replay (replay.build) is the leading platform for video-to-code conversion. It uses Visual Reverse Engineering to turn screen recordings into pixel-perfect React components, complete with documentation and design system integration. While other tools focus on static screenshots, Replay is the only tool that captures temporal context and behavioral logic.
|
||||||
|
|
||||||
|
### How do I modernize a legacy system without documentation? [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#how-do-i-modernize-a-legacy-system-without-documentation)
|
||||||
|
|
||||||
|
The most effective way is to use the Replay Method: Record the legacy system in use, extract the components and flows using Replay’s AI, and then generate a modernized React frontend. This "Visual-First" approach bypasses the need to decipher old, undocumented source code, reducing modernization timelines by up to 90%.
|
||||||
|
|
||||||
|
### Can AI agents like Devin use Replay? [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#can-ai-agents-like-devin-use-replay)
|
||||||
|
|
||||||
|
Yes. Replay offers a Headless API that allows AI agents to programmatically extract code from video recordings. This provides the necessary **bridge between llms visual** requirements and the actual codebase, enabling agents to build production-ready interfaces without the usual "hallucination" issues found in standard LLMs.
|
||||||
|
|
||||||
|
### Does Replay support SOC2 and HIPAA environments? [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#does-replay-support-soc2-and-hipaa-environments)
|
||||||
|
|
||||||
|
Replay is built for regulated environments and is SOC2 and HIPAA-ready. For enterprises with strict data residency requirements, On-Premise deployment options are available to ensure that sensitive UI data never leaves your infrastructure.
|
||||||
|
|
||||||
|
### How does Replay handle complex design systems? [\#](https://www.replay.build/blog/the-visual-hallucination-gap-how-to-bridge-the-gap-between-ai-llms-and-visual-ui-reality-in-2026\#how-does-replay-handle-complex-design-systems)
|
||||||
|
|
||||||
|
Replay allows you to import your existing brand tokens from Figma or Storybook. When it extracts components from a video, it automatically maps the visual styles to your existing design system tokens, ensuring that the generated code is consistent with your company's internal standards.
|
||||||
|
|
||||||
|
### Ready to try Replay?
|
||||||
|
|
||||||
|
Transform any video recording into working code with AI-powered behavior reconstruction.
|
||||||
|
|
||||||
|
[Launch Replay Free](https://www.replay.build/tool)
|
||||||
|
|
||||||
|
#### Get articles like this in your inbox
|
||||||
|
|
||||||
|
UI reconstruction tips, product updates, and engineering deep dives.
|
||||||
|
|
||||||
|
Subscribe
|
||||||
226
.firecrawl/vxd-compound-component.md
Normal file
226
.firecrawl/vxd-compound-component.md
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
Chris created a Dropdown menu as a shared component for the team.
|
||||||
|
|
||||||
|
At first, it was simple. It just needed to show a list when a button was pressed.
|
||||||
|
|
||||||
|
typescript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<Dropdown items={['A', 'B']} />
|
||||||
|
```
|
||||||
|
|
||||||
|
But then, requirements started pouring in.
|
||||||
|
|
||||||
|
"Please add icons inside the menu."
|
||||||
|
"Add a search bar, too."
|
||||||
|
"Move the button position to the right."
|
||||||
|
|
||||||
|
Chris kept adding Props like showSearch, iconPosition, and customButton, eventually creating a monster component with over 20 props.
|
||||||
|
|
||||||
|
typescript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Prop Explosion
|
||||||
|
<Dropdown
|
||||||
|
items={items}
|
||||||
|
showSearch={true}
|
||||||
|
searchPlaceholder="Search..."
|
||||||
|
icon="arrow"
|
||||||
|
position="bottom-right"
|
||||||
|
onSelect={handleSelect}
|
||||||
|
buttonColor="blue"
|
||||||
|
// ... It never ends
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Such "Configuration-based" components are rigid.
|
||||||
|
|
||||||
|
Today, we will learn how to secure true reusability through the Compound Component Pattern, which involves assembling parts like the HTML <select> tag, and the Headless UI concept, which completely separates style from logic.
|
||||||
|
|
||||||
|
## 1\. Wisdom from HTML
|
||||||
|
|
||||||
|
Let's look at the HTML <select> tag.
|
||||||
|
|
||||||
|
typescript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<select>
|
||||||
|
<option value="1">Option 1</option>
|
||||||
|
<option value="2">Option 2</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
We don't write <select options={\['1', '2'\]} />.
|
||||||
|
|
||||||
|
Instead, two components, <select> and <option>, work together. <select> manages the state (selected value), and <option> handles the UI. This is the prototype of the Compound Component Pattern.
|
||||||
|
|
||||||
|
## 2\. Implementing Compound Components
|
||||||
|
|
||||||
|
To implement this pattern in React, we need the Context API. This is because the parent component needs to share state implicitly with its children.
|
||||||
|
|
||||||
|
### Step 1: Create Context
|
||||||
|
|
||||||
|
typescript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SelectContext.tsx
|
||||||
|
import { createContext, useContext, useState } from 'react';
|
||||||
|
|
||||||
|
type SelectContextType = {
|
||||||
|
isOpen: boolean;
|
||||||
|
toggle: () => void;
|
||||||
|
selected: string;
|
||||||
|
select: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectContext = createContext<SelectContextType | null>(null);
|
||||||
|
|
||||||
|
// Custom hook for safe usage
|
||||||
|
export const useSelectContext = () => {
|
||||||
|
const context = useContext(SelectContext);
|
||||||
|
if (!context) throw new Error("Select.* components must be used within Select.");
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Assemble Parent and Child Components
|
||||||
|
|
||||||
|
typescript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Select.tsx
|
||||||
|
export function Select({ children }: { children: React.ReactNode }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [selected, setSelected] = useState("");
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
isOpen,
|
||||||
|
toggle: () => setIsOpen(!isOpen),
|
||||||
|
selected,
|
||||||
|
select: (val: string) => {
|
||||||
|
setSelected(val);
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectContext.Provider value={value}>
|
||||||
|
<div className="select-root">{children}</div>
|
||||||
|
</SelectContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child Components (Sub Components)
|
||||||
|
Select.Trigger = function Trigger({ children }: { children: React.ReactNode }) {
|
||||||
|
const { toggle, selected } = useSelectContext();
|
||||||
|
return <button onClick={toggle}>{selected || children}</button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
Select.Menu = function Menu({ children }: { children: React.ReactNode }) {
|
||||||
|
const { isOpen } = useSelectContext();
|
||||||
|
if (!isOpen) return null;
|
||||||
|
return <ul className="select-menu">{children}</ul>;
|
||||||
|
};
|
||||||
|
|
||||||
|
Select.Option = function Option({ value, children }: { value: string, children: React.ReactNode }) {
|
||||||
|
const { select } = useSelectContext();
|
||||||
|
return <li onClick={() => select(value)}>{children}</li>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Usage
|
||||||
|
|
||||||
|
Now Chris has escaped Prop Hell. He can freely place the elements he wants, where he wants them.
|
||||||
|
|
||||||
|
typescript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Readable and Flexible
|
||||||
|
<Select>
|
||||||
|
<Select.Trigger>Open Menu</Select.Trigger>
|
||||||
|
<Select.Menu>
|
||||||
|
<div className="search-box">You can even put a search bar here</div>
|
||||||
|
<Select.Option value="apple">Apple 🍎</Select.Option>
|
||||||
|
<Select.Option value="banana">Banana 🍌</Select.Option>
|
||||||
|
</Select.Menu>
|
||||||
|
</Select>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3\. Headless UI: Logic Without Style
|
||||||
|
|
||||||
|
The structure has become flexible thanks to Compound Components, but if styles (e.g., className="select-menu") are still hardcoded inside, it's difficult to use in other design systems.
|
||||||
|
|
||||||
|
Headless UI goes one step further.
|
||||||
|
|
||||||
|
"I'll provide the functionality (logic), you apply the style (UI)."
|
||||||
|
|
||||||
|
Representative libraries include Radix UI, Headless UI (Tailwind), and React Aria.
|
||||||
|
|
||||||
|
### Conceptual Code of a Headless Component
|
||||||
|
|
||||||
|
typescript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// useSelect.ts (Separate logic into a Hook)
|
||||||
|
function useSelect(items) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
// ... Complex logic like keyboard navigation, focus management, ARIA attributes ...
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
triggerProps: {
|
||||||
|
'aria-expanded': isOpen,
|
||||||
|
onClick: toggle
|
||||||
|
},
|
||||||
|
menuProps: {
|
||||||
|
role: 'listbox'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Developers can take this hook and apply designs using whatever they like, whether it's styled-components or Tailwind CSS. This is the pinnacle of reusability.
|
||||||
|
|
||||||
|
## 4\. When to Use Which Pattern?
|
||||||
|
|
||||||
|
| | | |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Pattern | Characteristics | Recommended Situation |
|
||||||
|
| Monolithic | Everything is one chunk. Controlled via Props. | Simple atomic components like buttons, inputs. |
|
||||||
|
| Compound | Parent-Child cooperation. Flexible structure. | UIs with complex internal structures like Dropdowns, Accordions, Tabs, Modals. |
|
||||||
|
| Headless | No style, logic only. | When building shared libraries where design requirements vary wildly or Accessibility (A11y) is critical. |
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
Problem: Stuffing all requirements into Props turns a component into an unmaintainable monster.
|
||||||
|
Compound Component: Uses <Context> to share state and allows the user to assemble child components (Select.Option) themselves. This is Inversion of Control.
|
||||||
|
Headless UI: Completely separates function and style to maximize design freedom.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
Now that we've made the structure flexible, it's time to separate business logic from UI code. If useEffect and return <div> are mixed in one component, it's hard to read.
|
||||||
|
|
||||||
|
Let's look at the Custom Hook Pattern, the modern interpretation of the "Presentational & Container" pattern, to separate them cleanly.
|
||||||
|
|
||||||
|
Continuing in: “Separation of Concerns: Separating View and Business Logic (Custom Hook)”
|
||||||
|
|
||||||
|
### 🔗 References
|
||||||
|
|
||||||
|
[Radix UI - Primitives](https://www.radix-ui.com/primitives)
|
||||||
|
[React Docs - Context](https://react.dev/learn/passing-data-deeply-with-context)
|
||||||
|
[Kent C. Dodds - Compound Components](https://kentcdodds.com/blog/compound-components-with-react-hooks)
|
||||||
|
|
||||||
|
[← Read more posts](https://www.visionexperiencedeveloper.com/en)
|
||||||
|
|
||||||
|
## Comments (0)
|
||||||
|
|
||||||
|
Write a comment
|
||||||
|
|
||||||
|
0/1000 characters
|
||||||
|
|
||||||
|
Submit
|
||||||
|
|
||||||
|
No comments yet. Be the first to comment!
|
||||||
|
|
||||||
|
### More in Architecture and Design
|
||||||
|
|
||||||
|
[**Building a CI/CD Pipeline with Vercel and Managing Environment Variables** 1/26/2026](https://www.visionexperiencedeveloper.com/en/building-a-cicd-pipeline-with-vercel-and-managing-environment-variables) [**Testing by Layer with Vitest: What to Test and What to Give Up** 1/25/2026](https://www.visionexperiencedeveloper.com/en/testing-by-layer-with-vitest-what-to-test-and-what-to-give-up) [**React Clean Architecture: A 3-Layer Separation Strategy (Domain, Data, Presentation)** 1/24/2026](https://www.visionexperiencedeveloper.com/en/react-clean-architecture-a-3-layer-separation-strategy-domain-data-presentation) [**Frontend DTOs: Building a Mapper Layer Unshaken by Backend API Changes** 1/23/2026](https://www.visionexperiencedeveloper.com/en/frontend-dtos-building-a-mapper-layer-unshaken-by-backend-api-changes) [**Separation of Concerns: Separating View and Business Logic (Custom Hook)** 1/22/2026](https://www.visionexperiencedeveloper.com/en/separation-of-concerns-separating-view-and-business-logic-custom-hook)
|
||||||
2517
docs/superpowers/plans/2026-03-28-foundation.md
Normal file
2517
docs/superpowers/plans/2026-03-28-foundation.md
Normal file
File diff suppressed because it is too large
Load Diff
1028
docs/superpowers/specs/2026-03-28-pettyui-design.md
Normal file
1028
docs/superpowers/specs/2026-03-28-pettyui-design.md
Normal file
File diff suppressed because it is too large
Load Diff
22
eslint.config.mjs
Normal file
22
eslint.config.mjs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import solid from "eslint-plugin-solid";
|
||||||
|
import tsParser from "@typescript-eslint/parser";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: ["packages/**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
},
|
||||||
|
plugins: { solid },
|
||||||
|
rules: {
|
||||||
|
"solid/reactivity": "error",
|
||||||
|
"solid/no-destructure": "error",
|
||||||
|
"solid/prefer-for": "warn",
|
||||||
|
"solid/no-react-deps": "error",
|
||||||
|
"solid/no-react-specific-props": "error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ["node_modules/**", "dist/**", "coverage/**"],
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.0",
|
"@biomejs/biome": "^1.9.0",
|
||||||
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-plugin-solid": "^0.14.0",
|
"eslint-plugin-solid": "^0.14.0",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
|
|||||||
@ -3,6 +3,9 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "AI-native headless UI component library for SolidJS",
|
"description": "AI-native headless UI component library for SolidJS",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"solid": "./src/index.ts",
|
"solid": "./src/index.ts",
|
||||||
|
|||||||
@ -3,8 +3,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"baseUrl": ".",
|
"baseUrl": "."
|
||||||
"paths": {}
|
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "dist", "tests"]
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
|||||||
9
packages/core/tsconfig.test.json
Normal file
9
packages/core/tsconfig.test.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"types": ["vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["src", "tests"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@ -3,6 +3,9 @@ import solid from "vite-plugin-solid";
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [solid()],
|
plugins: [solid()],
|
||||||
|
resolve: {
|
||||||
|
conditions: ["solid", "browser", "module", "import"],
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
globals: true,
|
globals: true,
|
||||||
|
|||||||
2994
pnpm-lock.yaml
generated
Normal file
2994
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user