Skip to content
edmi-ui

Watchlist Item

Compact sidebar row with a colored letter tile, symbol, mono price and change.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/watchlist-item
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/watchlist-item
Terminal window
yarn dlx shadcn@latest add @edmi-ui/watchlist-item
Terminal window
bunx shadcn@latest add @edmi-ui/watchlist-item

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/watchlist-item
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/watchlist-item
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/watchlist-item
Terminal window
bunx shadcn-vue@latest add @edmi-ui/watchlist-item

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/watchlist-item.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/watchlist-item.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/watchlist-item.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/watchlist-item.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { WatchlistItem } from "@/components/watchlist-item/watchlist-item";
<div className="flex w-60 flex-col gap-1.5">
<WatchlistItem
href="#mag4"
symbol="MAG4"
price="1.0000"
change="+2.38%"
color="var(--chart-1)"
active
/>
<WatchlistItem
href="#aifr"
symbol="AIFR"
price="1.0104"
change="+0.84%"
color="var(--chart-4)"
/>
<WatchlistItem
href="#atls"
symbol="ATLS"
price="0.9893"
change="−0.64%"
color="var(--chart-3)"
/>
</div>
<script setup lang="ts">
import { WatchlistItem } from "@/components/ui/watchlist-item";
</script>
<template>
<div class="flex w-60 flex-col gap-1.5">
<WatchlistItem href="#mag4" symbol="MAG4" price="1.0000" change="+2.38%" color="var(--chart-1)" active />
<WatchlistItem href="#aifr" symbol="AIFR" price="1.0104" change="+0.84%" color="var(--chart-4)" />
<WatchlistItem href="#atls" symbol="ATLS" price="0.9893" change="−0.64%" color="var(--chart-3)" />
</div>
</template>
<script lang="ts">
import { WatchlistItem } from "$lib/components/ui/watchlist-item";
</script>
<div class="flex w-60 flex-col gap-1.5">
<WatchlistItem href="#mag4" symbol="MAG4" price="1.0000" change="+2.38%" color="var(--chart-1)" active />
<WatchlistItem href="#aifr" symbol="AIFR" price="1.0104" change="+0.84%" color="var(--chart-4)" />
<WatchlistItem href="#atls" symbol="ATLS" price="0.9893" change="−0.64%" color="var(--chart-3)" />
</div>
registry/blocks/watchlist-item/watchlist-item.tsx
"use client";
import { cn } from "cn";
import type * as React from "react";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
const isDown = (s: string) => /^[-−–]/.test(s.trim());
// Compact sidebar row: colored letter tile, symbol, mono price and change.
function WatchlistItem({
className,
symbol,
price,
change,
color = "var(--chart-1)",
letter,
active,
elevation,
...props
}: Omit<React.ComponentProps<"a">, "children"> & {
symbol: string;
price: string;
/** Signed percentage, e.g. `+2.38%`. */
change: string;
/** Tile background: any CSS color, default `var(--chart-1)`. */
color?: string;
letter?: string;
active?: boolean;
/** ✦ depth of the active row (raised +1 / floating +2 bevel it). */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "control");
return (
<a
data-slot="watchlist-item"
data-active={active ? "" : undefined}
className={cn(
"flex h-10 items-center gap-2.5 rounded-lg px-2.5 text-[13.5px] text-sidebar-foreground outline-none hover:bg-sidebar-accent focus-visible:outline-2 focus-visible:outline-ring",
"data-[active]:bg-sidebar-accent data-[active]:font-medium data-[active]:shadow-[inset_0_0_0_1px_var(--sidebar-border)]",
level === "raised" &&
"border border-transparent data-[active]:border-transparent data-[active]:shadow-raised",
level === "floating" &&
"border border-transparent data-[active]:border-transparent data-[active]:shadow-floating",
className,
)}
{...props}
>
<span
className="inline-flex size-[26px] items-center justify-center rounded-[7px] text-xs font-semibold text-primary-foreground"
style={{ background: color }}
>
{letter ?? symbol.charAt(0)}
</span>
<span className="min-w-0 flex-1 truncate">{symbol}</span>
<span className="font-mono text-[11.5px] text-muted-foreground">
{price}
</span>
<span
className={cn(
"font-mono text-[11.5px]",
isDown(change) ? "text-destructive-text" : "text-success-text",
)}
>
{change}
</span>
</a>
);
}
export { WatchlistItem };
registry/ui/watchlist-item/WatchlistItem.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { computed } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'
const props = withDefaults(defineProps<{
symbol: string
price: string
/** Signed percentage, e.g. `+2.38%`. */
change: string
/** Tile background: any CSS color, default `var(--chart-1)`. */
color?: string
letter?: string
active?: boolean
/** ✦ depth of the active row (raised +1 / floating +2 bevel it) */
elevation?: Elevation
class?: HTMLAttributes['class']
}>(), {
color: 'var(--chart-1)',
elevation: undefined,
})
const level = useElevation(() => props.elevation, 'control')
const down = computed(() => /^[-−–]/.test(props.change.trim()))
</script>
<template>
<!-- Compact sidebar row: colored letter tile, symbol, mono price and change. -->
<a
data-slot="watchlist-item"
:data-active="active ? '' : undefined"
:class="cn(
'flex h-10 items-center gap-2.5 rounded-lg px-2.5 text-[13.5px] text-sidebar-foreground outline-none hover:bg-sidebar-accent focus-visible:outline-2 focus-visible:outline-ring',
'data-[active]:bg-sidebar-accent data-[active]:font-medium data-[active]:shadow-[inset_0_0_0_1px_var(--sidebar-border)]',
level === 'raised' && 'border border-transparent data-[active]:border-transparent data-[active]:shadow-raised',
level === 'floating' && 'border border-transparent data-[active]:border-transparent data-[active]:shadow-floating',
props.class,
)"
>
<span
class="inline-flex size-[26px] items-center justify-center rounded-[7px] text-xs font-semibold text-primary-foreground"
:style="{ background: color }"
>{{ letter ?? symbol.charAt(0) }}</span>
<span class="min-w-0 flex-1 truncate">{{ symbol }}</span>
<span class="font-mono text-[11.5px] text-muted-foreground">{{ price }}</span>
<span :class="cn('font-mono text-[11.5px]', down ? 'text-destructive-text' : 'text-success-text')">{{ change }}</span>
</a>
</template>
registry/ui/watchlist-item/index.ts
export { default as WatchlistItem } from "./WatchlistItem.vue";
src/lib/registry/ui/watchlist-item/index.ts
export { default as WatchlistItem } from "./watchlist-item.svelte";
src/lib/registry/ui/watchlist-item/watchlist-item.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes } from "svelte/elements";
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
let {
ref = $bindable(null),
class: className,
symbol,
price,
change,
color = "var(--chart-1)",
letter,
active,
elevation = "auto",
...restProps
}: WithElementRef<Omit<HTMLAnchorAttributes, "children">, HTMLAnchorElement> & {
symbol: string;
price: string;
/** Signed percentage, e.g. `+2.38%`. */
change: string;
/** Tile background: any CSS color, default `var(--chart-1)`. */
color?: string;
letter?: string;
active?: boolean;
/** ✦ depth of the active row (raised +1 / floating +2 bevel it). */
elevation?: Elevation;
} = $props();
const level = useElevation(() => elevation, "control");
const down = $derived(/^[-−–]/.test(change.trim()));
</script>
<!-- Compact sidebar row: colored letter tile, symbol, mono price and change. -->
<a
bind:this={ref}
data-slot="watchlist-item"
data-active={active ? "" : undefined}
class={cn(
"flex h-10 items-center gap-2.5 rounded-lg px-2.5 text-[13.5px] text-sidebar-foreground outline-none hover:bg-sidebar-accent focus-visible:outline-2 focus-visible:outline-ring",
"data-[active]:bg-sidebar-accent data-[active]:font-medium data-[active]:shadow-[inset_0_0_0_1px_var(--sidebar-border)]",
level.current === "raised" &&
"border border-transparent data-[active]:border-transparent data-[active]:shadow-raised",
level.current === "floating" &&
"border border-transparent data-[active]:border-transparent data-[active]:shadow-floating",
className
)}
{...restProps}
>
<span
class="inline-flex size-[26px] items-center justify-center rounded-[7px] text-xs font-semibold text-primary-foreground"
style:background={color}>{letter ?? symbol.charAt(0)}</span
>
<span class="min-w-0 flex-1 truncate">{symbol}</span>
<span class="font-mono text-[11.5px] text-muted-foreground">{price}</span>
<span class={cn("font-mono text-[11.5px]", down ? "text-destructive-text" : "text-success-text")}>{change}</span>
</a>

See the Elevation guide for the levels, roles and layered mode.

Flat by default. elevation raises only the active row: raised (+1) adds the bevel, floating (+2) the bevel plus one soft drop.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Part Prop Type Default
WatchlistItem symbol / price / change string required
color CSS color var(--chart-1)
letter string first letter of symbol
active boolean false
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"

Renders an <a>; pass href and other anchor props.

  • Uses the --sidebar-* tokens, so it fits inside the Sidebar block.