Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion component.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"preview/src/components/textarea",
"preview/src/components/skeleton",
"preview/src/components/card",
"preview/src/components/sheet"
"preview/src/components/sheet",
"preview/src/components/badge"
]
}
21 changes: 20 additions & 1 deletion preview/src/components/avatar/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ impl AvatarImageSize {
}
}

#[derive(Clone, Copy, PartialEq, Default)]
pub enum AvatarShape {
#[default]
Circle,
Rounded,
}

impl AvatarShape {
fn to_class(self) -> &'static str {
match self {
AvatarShape::Circle => "avatar-circle",
AvatarShape::Rounded => "avatar-rounded",
}
}
}

/// The props for the [`Avatar`] component.
#[derive(Props, Clone, PartialEq)]
pub struct AvatarProps {
Expand All @@ -37,6 +53,9 @@ pub struct AvatarProps {
#[props(default)]
pub size: AvatarImageSize,

#[props(default)]
pub shape: AvatarShape,

/// Additional attributes for the avatar element
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
Expand All @@ -51,7 +70,7 @@ pub fn Avatar(props: AvatarProps) -> Element {
document::Link { rel: "stylesheet", href: asset!("./style.css") }

avatar::Avatar {
class: "avatar {props.size.to_class()}",
class: "avatar {props.size.to_class()} {props.shape.to_class()}",
on_load: props.on_load,
on_error: props.on_error,
on_state_change: props.on_state_change,
Expand Down
12 changes: 10 additions & 2 deletions preview/src/components/avatar/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
flex-shrink: 0;
align-items: center;
justify-content: center;
border-radius: 3.40282e+38px;
color: var(--secondary-color-4);
cursor: pointer;
font-weight: 500;
Expand Down Expand Up @@ -52,13 +51,22 @@
font-size: 1.75rem;
}

/* Avatar shape */
.avatar-circle {
border-radius: 50%;
}

.avatar-rounded {
border-radius: 8px;
}

/* State-specific styles */
.avatar[data-state="loading"] {
animation: pulse 1.5s infinite ease-in-out;
}

.avatar[data-state="empty"] {
background: var(--primary-color-2);
background: var(--primary-color-7);
}

@keyframes pulse {
Expand Down
13 changes: 13 additions & 0 deletions preview/src/components/badge/component.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "Badge",
"description": "Show notifications, counts or status information on its children",
"authors": ["Evan Almloff"],
"exclude": ["variants", "docs.md", "component.json"],
"cargoDependencies": [
{
"name": "dioxus-primitives",
"git": "https://github.com/DioxusLabs/components"
}
],
"globalAssets": ["../../../assets/dx-components-theme.css"]
}
19 changes: 19 additions & 0 deletions preview/src/components/badge/component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use dioxus::prelude::*;
use dioxus_primitives::badge::{self, BadgeProps};

#[component]
pub fn Badge(props: BadgeProps) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: asset!("./style.css") }

badge::Badge {
count: props.count,
overflow_count: props.overflow_count,
dot: props.dot,
show_zero: props.show_zero,
color: props.color,
attributes: props.attributes,
{props.children}
}
}
}
10 changes: 10 additions & 0 deletions preview/src/components/badge/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Badges are used as a small numerical value or status descriptor for its children elements.
Badge will be hidden when count is 0, but we can use show_zero to show it.

## Component Structure

```rust
Badge {
{children}
}
```
2 changes: 2 additions & 0 deletions preview/src/components/badge/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod component;
pub use component::*;
41 changes: 41 additions & 0 deletions preview/src/components/badge/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.badge-example {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
}

.badge-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}

.badge-label {
color: var(--secondary-color-4);
font-size: 0.875rem;
}

.badge {
position: absolute;
display: inline-flex;
min-width: 20px;
height: 20px;
align-items: center;
justify-content: center;
border-radius: 10px;
background-color: var(--badge-color);
box-shadow: 0 0 0 1px var(--primary-color-2);
font-size: 12px;
transform: translate(-50%, -50%);
}

.badge[padding="true"] {
padding: 0 8px;
}

.badge[dot="true"] {
min-width: 8px;
height: 8px;
}
86 changes: 86 additions & 0 deletions preview/src/components/badge/variants/main/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use dioxus::prelude::*;

use super::super::component::*;
use crate::components::avatar::*;

#[component]
pub fn Demo() -> Element {
rsx! {
div {
class: "badge-example",

div {
class: "badge-item",
p { class: "badge-label", "Basic" }
Badge {
count: 5,
Avatar {
size: AvatarImageSize::Medium,
shape: AvatarShape::Rounded,
aria_label: "Space item",
}
}
}

div {
class: "badge-item",
p { class: "badge-label", "Show Zero" }

Badge {
count: 0,
show_zero: true,
Avatar {
size: AvatarImageSize::Medium,
shape: AvatarShape::Rounded,
aria_label: "Space item",
}
}
}

div {
class: "badge-item",
p { class: "badge-label", "Overflow" }

Badge {
count: 100,
overflow_count: 99,
Avatar {
size: AvatarImageSize::Medium,
shape: AvatarShape::Rounded,
aria_label: "Space item",
}
}
}

div {
class: "badge-item",
p { class: "badge-label", "Colorful" }

Badge {
count: 7,
color: String::from("52c41a"),
Avatar {
size: AvatarImageSize::Medium,
shape: AvatarShape::Rounded,
aria_label: "Space item",
}
}
}

div {
class: "badge-item",
p { class: "badge-label", "As Dot" }

Badge {
count: 5,
dot: true,
Avatar {
size: AvatarImageSize::Medium,
shape: AvatarShape::Rounded,
aria_label: "Space item",
}
}
}
}
}
}
1 change: 1 addition & 0 deletions preview/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ examples!(
alert_dialog,
aspect_ratio,
avatar,
badge,
button,
calendar[simple, internationalized, range, multi_month, unavailable_dates],
checkbox,
Expand Down
92 changes: 92 additions & 0 deletions primitives/src/badge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! Defines the [`Badge`] component
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since badge doesn't have any special accessibility behavior (it is just a span), the component should be defined only in the component.rs in the preview instead of in the primitives


use dioxus::prelude::*;

const DEF_COLOR: &str = "EB5160";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the default color should be defined in css as an existing or new css variable here: https://github.com/DioxusLabs/components/blob/main/preview/assets/dx-components-theme.css#L31


/// The props for the [`Badge`] component.
#[derive(Props, Clone, PartialEq)]
pub struct BadgeProps {
/// Number to show in badge
pub count: u32,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the current version is useful with notifications, I would like to generalize the component outside of that. Badges should also be usable standalone with whatever text the user passes in as a child. For example this github UI should be expressible as a badge component with Badge { "enhancement" }
Screenshot 2026-01-08 at 8 19 25 AM

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What name is recommended for the notification component then? If I understand correctly, we can't reuse the same component for two cases.

Copy link
Member

@ealmloff ealmloff Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could re-use most of it. The only part that isn't applicable is the positioning, but that can be configured from the props. In shadcn, notifications can be just a specific application of badges. An example from https://shadcnstudio.com/docs/components/badge:

import { ShoppingCartIcon } from 'lucide-react'

import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'

const BadgeCartDemo = () => {
  return (
    <div className='relative w-fit'>
      <Avatar className='size-9 rounded-sm'>
        <AvatarFallback className='rounded-sm'>
          <ShoppingCartIcon className='size-5' />
        </AvatarFallback>
      </Avatar>
      <Badge className='absolute -top-2.5 -right-2.5 h-5 min-w-5 px-1 tabular-nums'>8</Badge>
    </div>
  )
}

export default BadgeCartDemo


/// Max count to show
#[props(default = u32::MAX)]
pub overflow_count: u32,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another notification specific prop


/// Whether to display a dot instead of count
#[props(default = false)]
pub dot: bool,

/// Whether to show badge when count is zero
#[props(default = false)]
pub show_zero: bool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another notification specific prop


/// Customize Badge color (as HEX)
#[props(default = String::from(DEF_COLOR))]
pub color: String,

/// Additional attributes to extend the badge element
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,

/// The children of the badge element
pub children: Element,
}

/// # Badge
///
/// The [`Badge`] component displays a small badge to the top-right of its child(ren).
///
/// ## Example
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_primitives::badge::Badge;
/// use dioxus_primitives::avatar::*;
/// #[component]
/// fn Demo() -> Element {
/// rsx! {
/// Badge {
/// count: 100,
/// overflow_count: 99,
/// Avatar {
/// aria_label: "Space item",
/// }
/// }
/// }
/// }
/// ```
#[component]
pub fn Badge(props: BadgeProps) -> Element {
let text = if props.dot {
String::default()
} else if props.overflow_count < props.count {
format!("{}+", props.overflow_count)
} else {
format!("{}", props.count)
};

let add_padding = text.chars().count() > 1;
let color = if u32::from_str_radix(&props.color, 16).is_ok() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other valid syntaxes for color strings in css. We don't need to parse it here and quote it with #

props.color
} else {
DEF_COLOR.to_string()
};

rsx! {
span {
{props.children}

if props.count > 0 || props.show_zero {
span {
class: "badge",
style: "--badge-color: #{color}",
"padding": if add_padding { true },
"dot": if props.dot { true },
..props.attributes,
{text}
}
}
}
}
}
Loading
Loading