# detaxine-ui
A Leptos + Tailwind CSS component library compiled to WebAssembly. Build modern, reactive web UIs with a full set of accessible, themeable components — no JavaScript framework required.
[](https://crates.io/crates/detaxine-ui)
[](https://docs.rs/detaxine-ui)
[](LICENSE-MIT)
[](LICENSE-APACHE)
## Live Demo
[https://elonaire.github.io/detaxine-ui/](https://elonaire.github.io/detaxine-ui/)
---
## Installation
### New Project
Install the CLI and let it scaffold everything for you:
```bash
cargo install detaxine-ui-cli
dtx init my-app
cd my-app
trunk serve
```
`dtx init` creates a ready-to-run Leptos + Trunk project with `detaxine-ui`
as a dependency, `input.css` configured, and a Tailwind binary in place.
Import re-exported crates directly from `detaxine_ui` in your code:
```rust
// Instead of: use leptos::prelude::*;
use detaxine_ui::leptos::prelude::*;
// Instead of: use leptos_router::components::Router;
use detaxine_ui::leptos_router::components::Router;
// Instead of: use chrono::{Local, DateTime};
use detaxine_ui::chrono::{Local, DateTime};
```
Consumers who already depend on these crates directly can still do so —
Cargo's deduplication ensures only one copy is compiled as long as the
versions are compatible.
### Existing Project
`Coming soon`
---
## Prerequisites
- [Rust](https://rustup.rs/) (stable)
- [Trunk](https://trunkrs.dev/) — WASM bundler for Leptos
```bash
cargo install trunk
rustup target add wasm32-unknown-unknown
```
---
## Quick Start
```rust
use detaxine_ui::{components::actions::button::BasicButton, leptos::prelude::*, icondata::AiCheckCircleOutlined};
#[component]
fn App() -> impl IntoView {
view! {
<BasicButton
button_text="Confirm"
icon=Some(AiCheckCircleOutlined)
icon_before=true
style_ext="bg-primary text-white hover:bg-secondary px-6 py-2.5"
onclick=Callback::new(|_| leptos::logging::log!("clicked"))
/>
}
}
fn main() {
mount_to_body(App)
}
```
---
## Components
### Actions
| `BasicButton` | Button with optional icon, disabled state, and custom styles |
| `ButtonGroup` | Inline group of buttons with shared styles and rounded ends |
| `Carousel` | Sliding panel carousel with navigation and indicators |
### Data Display
| `Badge` | Overlaid count or status indicator anchored to a child element |
| `Chip` | Removable tag with color temperature variants |
| `LabelTag` | Static color-coded label |
| `Timeline` | Vertical list of timestamped steps with icon, image, or circle heads |
| `DataTable` | Sortable, paginated table with optional row-level edit and delete actions |
| `Pagination` | Page navigation control emitting page-change callbacks |
### Feedback
| `BasicModal` | Portal-based dialog with contextual icons and configurable footer actions |
| `Popover` | Viewport-aware floating panel with auto-alignment and route-change close |
| `ProgressBar` | Horizontal progress bar with determinate and indeterminate modes |
| `CircularProgress` | Circular progress ring with optional percentage label |
| `Spinner` | Animated SVG loading spinner with optional full-screen backdrop |
### Forms
| `InputField` | All standard HTML input types via the `InputFieldType` enum |
| `CustomFileInput` | Styled file picker with chip-based selected file list |
| `Textarea` | Multi-line text input |
| `SelectInput` | Native select dropdown |
| `CustomSelectInput` | Searchable, chip-based single or multi-select |
| `CheckboxInputField` | Single checkbox input |
| `CheckboxGroup` | Grouped checkboxes with shared HashSet selection state |
| `RadioInputField` | Single radio input |
| `RadioInputGroup` | Grouped radio inputs with shared selection state |
| `ToggleSwitch` | Boolean toggle built on a hidden checkbox |
| `DatePicker` | Calendar date picker with min/max and disabled date support |
| `RichTextEditor` | Contenteditable rich text editor with formatting toolbar |
| `ReactiveForm` | Form wrapper that auto-submits whenever all fields are valid |
### Navigation
| `Breadcrumbs` | Route-derived breadcrumb trail with custom label support |
| `Panel` | Collapsible panel with a clickable title bar |
| `Collapse` | Accordion group of panels with optional single-open enforcement |
| `Tabs` | Scrollable tabbed view with slot-based content |
| `Stepper` | Multi-step form wizard with per-step validation and linear mode |
---
## Theming
`detaxine-ui` uses CSS custom properties for its color system. Override them in your own CSS to match your brand:
```css
@theme {
--color-primary: #3b82f6;
--color-secondary: #6366f1;
--color-danger: #ef4444;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-info: #0ea5e9;
--color-light-gray: #e5e7eb;
--color-mid-gray: #9ca3af;
--color-contrast-white: #ffffff;
--color-navy: #0f172a;
}
```
All components reference these variables via Tailwind utilities (`bg-primary`, `text-danger`, etc.) so a single override updates the entire library.
---
## Style Extension
Every component exposes one or more style extension props so you can apply any Tailwind utility without forking the library:
| `style_ext` | The root or primary element |
| `ext_input_styles` | The input or select element |
| `ext_wrapper_styles` | The outer wrapper div |
| `ext_label_styles` | The label element |
| `container_style_ext` | The modal panel |
Example — making a full-width danger button:
```rust
use detaxine_ui::{components::actions::button::BasicButton, leptos::prelude::*, icondata::AiCheckCircleOutlined};
#[component]
fn Example() -> impl IntoView {
view! {
<BasicButton
button_text="Delete"
style_ext="w-full bg-danger text-white hover:bg-danger/80"
/>
}
}
```
---
## Form Handling
`ReactiveForm` wraps your fields and fires a native `submit` event automatically whenever `checkValidity()` returns true, on every `input` or `change` event. Read form data with the standard `FormData` API:
```rust
use detaxine_ui::{
components::forms::{
checkbox::{CheckboxGroup, CheckboxOption},
ReactiveForm, InputField, InputFieldType,
},
leptos::prelude::*,
};
use std::collections::HashSet;
#[component]
fn Example() -> impl IntoView {
let selected = RwSignal::new(HashSet::new());
let form_ref = NodeRef::new();
let handle_submit = move |ev: SubmitEvent| {
ev.prevent_default();
let target = ev.target()
.and_then(|t| t.dyn_into::<HtmlFormElement>().ok());
if let Some(form) = target {
if let Ok(form_data) = FormData::new_with_form(&form) {
let name = form_data.get("name").as_string().unwrap_or_default();
leptos::logging::log!("name: {}", name);
}
}
};
view! {
<ReactiveForm form_ref=form_ref on:submit=handle_submit>
<CheckboxGroup
legend="Interests"
name="interests"
options=RwSignal::new(vec![
CheckboxOption::new("rust", "Rust", None),
CheckboxOption::new("leptos", "Leptos", None),
])
selected_values=selected
/>
<InputField field_type=InputFieldType::Email name="email" label="Email" required=true />
</ReactiveForm>
}
}
```
---
## Modal Setup
`BasicModal` portals its content into a `#modal-root` element. Add this to your `index.html` before the closing `</body>` tag or to your root component:
```html
<div id="modal-root"></div>
```
---
## Router Requirement ⚠️
`Popover`, `DataTable` (which uses `Popover` internally), and `Breadcrumbs` all call `use_location()` from `leptos_router`. Wrap your app in a `<Router>` or else these components will panic at runtime:
```rust
use leptos_router::components::Router;
fn main() {
mount_to_body(|| view! {
<Router>
// ... your root component
</Router>
})
}
```
---
## Rich Text Editor — Image Upload
The `RichTextEditor` defaults to base64 data URLs for image insertion. Supply a custom `on_image_insert` callback to upload to your own storage:
```rust
use detaxine_ui::{
components::content::richtext_editor::{ExtraFormatingOption, RichTextEditor},
leptos::prelude::*,
web_sys::File
};
use std::pin::Pin;
#[component]
fn Example() -> impl IntoView {
let content = RwSignal::new("<p>Hello world!</p>".to_string());
view! {
<RichTextEditor
initial_content=content
id_attr="editor"
name="body"
placeholder="Write something..."
extra_formating_options=vec![
ExtraFormatingOption::Heading,
ExtraFormatingOption::Lists,
ExtraFormatingOption::CodeBlock,
ExtraFormatingOption::ImageUpload,
]
on_image_insert=Callback::new(move |file: File| {
Box::pin(async move {
// upload to S3, Cloudflare R2, etc. and return the URL
Some("https://cdn.example.com/image.png".to_string())
}) as Pin<Box<dyn Future<Output = Option<String>>>>
})
/>
}
}
```
---
## Contributing
Contributions are welcome. Please open an issue before submitting a pull request for significant changes.
1. Fork the repository
2. Create a feature branch: `git checkout -b feat/my-component`
3. Commit your changes: `git commit -m "feat: add MyComponent"`
4. Push and open a pull request
---
## License
This project is licensed under both the MIT license and the Apache License (Version 2.0).
MIT — see [LICENSE-MIT](LICENSE-MIT) for details.
APACHE — see [LICENSE-APACHE](LICENSE-APACHE) for details.