diff --git a/packages/components/src/components/Username/Username.mdx b/packages/components/src/components/Username/Username.mdx
new file mode 100644
index 00000000..8b6bb557
--- /dev/null
+++ b/packages/components/src/components/Username/Username.mdx
@@ -0,0 +1,196 @@
+import {
+ Meta,
+ Story,
+ Props,
+ Status,
+} from '../../../../../.storybook/components';
+
+import * as Stories from './Username.stories';
+
+
+
+# Username
+
+
+
+Username is used when an interface needs to reference an internal system user.
+It presents a set of identifying attributes — full name, login, and site — in a consistent layout.
+
+## Import
+
+```tsx
+import {
+ Username,
+ UsernamePrimary,
+ UsernameSecondary,
+ UsernameSecondaryHint,
+} from '@koobiq/react-components';
+```
+
+## Usage
+
+Pass a `userInfo` object to render the default template automatically.
+
+
+
+## Props
+
+
+
+## Mode
+
+The `mode` prop controls the layout of the name parts.
+
+- `inline` — displays primary and secondary on one line. Both parts have ellipsis. This is the default.
+- `stacked` — displays primary and secondary on separate lines.
+- `text` — renders as inline text with no ellipsis, suitable for use inside a paragraph.
+
+
+
+## Type
+
+The `type` prop sets the visual color style.
+
+- `default` — primary text in contrast color, secondary in secondary-contrast color.
+- `error` — all parts in error color.
+- `accented` — same colors as `default`, but primary text uses bold weight. Use for typographic emphasis without introducing color.
+- `inherit` — all parts inherit color from the parent element.
+
+
+
+## Compact
+
+When `isCompact` is `true`, primary and secondary are collapsed into a single line:
+the full name (or login if no full name is available) is shown, followed by the site in parentheses.
+
+
+
+## Login only
+
+When `firstName` or `lastName` is absent from `userInfo`, the component falls back to showing
+the `login` value as the primary content.
+
+
+
+## With site
+
+The optional `site` field renders as a hint after the login.
+
+
+
+## Custom view
+
+Provide `children` to take full control of the output.
+The default template is skipped entirely; `mode` and `type` still apply to the root element.
+
+Use `UsernamePrimary`, `UsernameSecondary`, and `UsernameSecondaryHint` to keep the same visual
+style as the default template.
+
+
+
+## As link
+
+Use `type="inherit"` when placing `Username` inside a link so it inherits the link's color.
+
+
+
+## Name format
+
+The `fullNameFormat` prop controls how the full name is assembled from `userInfo`.
+
+| Character | Field | Behavior |
+| --------- | ------------ | ------------------------------------------------- |
+| `l` | `lastName` | Full value, or initial + `.` when followed by `.` |
+| `f` | `firstName` | Full value, or initial + `.` when followed by `.` |
+| `m` | `middleName` | Full value, or initial + `.` when followed by `.` |
+| `.` | — | Separator marker — not emitted literally |
+
+The default format `'lf.m.'` produces `"Root M. A."` for a user with
+`lastName: 'Root'`, `firstName: 'Maxwell'`, `middleName: 'Alan'`.
+
+Empty fields are silently skipped without leaving stray punctuation.
+
+Use the exported `formatUsername` utility to get the same string in non-JSX contexts:
+
+```tsx
+import { formatUsername } from '@koobiq/react-components';
+
+const name = formatUsername(
+ { firstName: 'Maxwell', middleName: 'Alan', lastName: 'Root' },
+ 'lf.m.'
+);
+// → "Root M. A."
+```
+
+## Custom formatter
+
+The `formatter` prop lets you swap the name-formatting function.
+Pass `formatUsernameCustom` (or any function with the same signature) to use a different format style.
+
+`formatUsernameCustom` uses a richer format string where character case determines full vs initial output,
+and non-field characters pass through literally:
+
+| Character | Field | Output |
+| --------- | ------------ | -------------------------------------------- |
+| `L` | `lastName` | Full value |
+| `l` | `lastName` | First character only |
+| `F` | `firstName` | Full value |
+| `f` | `firstName` | First character only |
+| `M` | `middleName` | Full value |
+| `m` | `middleName` | First character only |
+| any other | — | Emitted literally (spaces, dots, separators) |
+
+The default format for `formatUsernameCustom` is `'L f. m.'`, which produces the same
+`"Root M. A."` output as the default formatter but with literal dots and spaces from the
+format string rather than token-joining.
+
+
+
+```tsx
+import { formatUsernameCustom } from '@koobiq/react-components';
+
+;
+// Primary renders: "Maxwell Root"
+```
+
+A custom `mapping` can be passed by wrapping the function:
+
+```tsx
+ formatUsernameCustom(info, fmt, myMapping)}
+ fullNameFormat="F L"
+/>
+```
+
+## buildUsernameText
+
+`buildUsernameText` assembles a plain-text string that mirrors what `Username` renders.
+Use it for `aria-label` values or search/filter logic.
+
+It takes a pre-formatted `name` string (the output of `formatUsername` or `formatUsernameCustom`)
+plus optional `login` and `site`. By default `site` is wrapped in parentheses.
+
+```tsx
+import { buildUsernameText, formatUsername } from '@koobiq/react-components';
+
+const name = formatUsername(userInfo, 'lf.m.');
+buildUsernameText({ name, login: userInfo.login, site: userInfo.site });
+// → "Root M. A. mroot (corp)"
+```
+
+Use `formatLogin` and `formatSite` options to customise how those segments appear:
+
+```tsx
+// Remove parentheses from site (useful for search comparisons)
+buildUsernameText({ name, login, site }, { formatSite: (s) => s });
+// → "Root M. A. mroot corp"
+
+// Wrap login in brackets
+buildUsernameText({ name, login, site }, { formatLogin: (l) => `[${l}]` });
+// → "Root M. A. [mroot] (corp)"
+```
diff --git a/packages/components/src/components/Username/Username.module.css b/packages/components/src/components/Username/Username.module.css
new file mode 100644
index 00000000..ab576792
--- /dev/null
+++ b/packages/components/src/components/Username/Username.module.css
@@ -0,0 +1,94 @@
+@import url('../../styles/mixins.css');
+
+/* ── Root ─────────────────────────────────────────────────────── */
+
+.base {
+ --username-color-primary: ;
+ --username-color-secondary: ;
+ --username-color-hint: ;
+
+ @mixin typography text-normal;
+
+ display: inline-flex;
+ max-inline-size: 100%;
+ box-sizing: border-box;
+}
+
+/* ── Mode modifiers ───────────────────────────────────────────── */
+
+.stacked {
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.inline {
+ flex-direction: row;
+ align-items: baseline;
+ gap: var(--kbq-size-xxs);
+}
+
+.text {
+ display: inline;
+}
+
+/* ── Type modifiers — populate custom properties ─────────────── */
+
+.default {
+ --username-color-primary: var(--kbq-foreground-contrast);
+ --username-color-secondary: var(--kbq-foreground-contrast-secondary);
+ --username-color-hint: var(--kbq-foreground-contrast-secondary);
+}
+
+.error {
+ --username-color-primary: var(--kbq-foreground-error);
+ --username-color-secondary: var(--kbq-foreground-error);
+ --username-color-hint: var(--kbq-foreground-error);
+}
+
+.accented {
+ --username-color-primary: var(--kbq-foreground-contrast);
+ --username-color-secondary: var(--kbq-foreground-contrast-secondary);
+ --username-color-hint: var(--kbq-foreground-contrast-secondary);
+}
+
+.inherit {
+ --username-color-primary: inherit;
+ --username-color-secondary: inherit;
+ --username-color-hint: inherit;
+
+ font: inherit;
+ display: inherit;
+}
+
+/* ── Sub-element classes ─────────────────────────────────────── */
+
+.primary {
+ @mixin ellipsis;
+
+ color: var(--username-color-primary);
+}
+
+.secondary {
+ @mixin ellipsis;
+
+ color: var(--username-color-secondary);
+}
+
+.secondaryHint {
+ color: var(--username-color-hint);
+}
+
+/* accented type: primary uses bold weight */
+
+.accented .primary {
+ @mixin typography text-normal-strong;
+}
+
+/* text mode: remove ellipsis so content flows as prose */
+
+.text .primary,
+.text .secondary {
+ overflow: visible;
+ white-space: normal;
+ text-overflow: unset;
+}
diff --git a/packages/components/src/components/Username/Username.stories.tsx b/packages/components/src/components/Username/Username.stories.tsx
new file mode 100644
index 00000000..c9b4bc8f
--- /dev/null
+++ b/packages/components/src/components/Username/Username.stories.tsx
@@ -0,0 +1,231 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { FlexBox } from '../FlexBox';
+import { Link } from '../Link';
+
+import {
+ Username,
+ UsernamePrimary,
+ UsernameSecondary,
+ UsernameSecondaryHint,
+ type UsernameProps,
+ formatUsernameCustom,
+ usernamePropMode,
+ usernamePropType,
+} from './index.js';
+
+const defaultUserInfo = {
+ firstName: 'Maxwell',
+ middleName: 'Alan',
+ lastName: 'Root',
+ login: 'mroot',
+ site: 'corp',
+};
+
+const meta = {
+ title: 'Components/Username',
+ component: Username,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['status:new', 'date:2026-07-22'],
+ args: {
+ userInfo: defaultUserInfo,
+ },
+ argTypes: {
+ mode: { control: { type: 'select' }, options: usernamePropMode },
+ type: { control: { type: 'select' }, options: usernamePropType },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Base: Story = {
+ render: (args) => ,
+};
+
+export const Mode: Story = {
+ render: (args) => (
+
+ {usernamePropMode.map((mode) => (
+
+
+ mode = {mode}
+
+
+
+ ))}
+
+ ),
+};
+
+export const Type: Story = {
+ render: (args) => (
+
+ {usernamePropType.map((type) => (
+
+
+ type = {type}
+
+ {type === 'inherit' ? (
+
+
+
+ ) : (
+
+ )}
+
+ ))}
+
+ ),
+};
+
+export const Compact: Story = {
+ render: (args) => (
+
+
+
+ isCompact = false
+
+
+
+
+
+ isCompact = true
+
+
+
+
+ ),
+};
+
+export const OnlyLogin: Story = {
+ render: (args) => (
+
+ ),
+};
+
+export const WithSite: Story = {
+ render: (args) => (
+
+
+
+ non-compact with site
+
+
+
+
+
+ compact with site
+
+
+
+
+ ),
+};
+
+export const CustomView: Story = {
+ render: (args) => (
+
+ Root M. A.
+
+ [mroot]
+ (corp)
+
+
+ ),
+};
+
+export const AsLink: Story = {
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const CustomFormatter: Story = {
+ render: (args) => (
+
+
+
+ formatUsername (default) — format: 'lf.m.'
+
+
+
+
+
+ formatUsernameCustom — format: 'L f. m.'
+
+
+
+
+
+ formatUsernameCustom — format: 'F L'
+
+
+
+
+ ),
+};
diff --git a/packages/components/src/components/Username/Username.test.tsx b/packages/components/src/components/Username/Username.test.tsx
new file mode 100644
index 00000000..b037b9f1
--- /dev/null
+++ b/packages/components/src/components/Username/Username.test.tsx
@@ -0,0 +1,330 @@
+import { createRef } from 'react';
+
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+
+import {
+ Username,
+ UsernamePrimary,
+ buildUsernameText,
+ formatUsername,
+ formatUsernameCustom,
+ usernamePropMode,
+ usernamePropType,
+} from './index.js';
+
+const fullProfile = {
+ firstName: 'Maxwell',
+ middleName: 'Alan',
+ lastName: 'Root',
+ login: 'mroot',
+ site: 'corp',
+};
+
+describe('Username', () => {
+ const baseProps = { 'data-testid': 'username' };
+ const getRoot = () => screen.getByTestId('username');
+
+ it('should forward a ref to the root span element', () => {
+ const ref = createRef();
+ const { container } = render();
+ expect(ref.current).toBe(container.firstElementChild);
+ });
+
+ it('should merge a custom className with component classes', () => {
+ render();
+ expect(getRoot()).toHaveClass('custom');
+ });
+
+ it('should spread additional HTML props onto the root element', () => {
+ render();
+ expect(getRoot()).toHaveAttribute('aria-label', 'User');
+ });
+
+ describe('default data attributes', () => {
+ it('should set data-mode to "inline" by default', () => {
+ render();
+ expect(getRoot()).toHaveAttribute('data-mode', 'inline');
+ });
+
+ it('should set data-type to "default" by default', () => {
+ render();
+ expect(getRoot()).toHaveAttribute('data-type', 'default');
+ });
+
+ it('should not set data-compact by default', () => {
+ render();
+ expect(getRoot()).not.toHaveAttribute('data-compact');
+ });
+ });
+
+ describe('prop reflection', () => {
+ it.each(usernamePropMode)('should set data-mode to "%s"', (mode) => {
+ render();
+ expect(getRoot()).toHaveAttribute('data-mode', mode);
+ });
+
+ it.each(usernamePropType)('should set data-type to "%s"', (type) => {
+ render();
+ expect(getRoot()).toHaveAttribute('data-type', type);
+ });
+
+ it('should set data-compact when isCompact is true', () => {
+ render();
+ expect(getRoot()).toHaveAttribute('data-compact');
+ });
+ });
+
+ describe('default template rendering (non-compact)', () => {
+ it('should render the formatted full name in Primary when firstName and lastName are present', () => {
+ render();
+ expect(screen.getByText('Root M. A.')).toBeInTheDocument();
+ });
+
+ it('should apply the fullNameFormat prop to the rendered name', () => {
+ render();
+ expect(screen.getByText('Root M.')).toBeInTheDocument();
+ });
+
+ it('should render login in Secondary when login is provided', () => {
+ render();
+ const secondary = document.querySelector('[class*="secondary"]');
+ expect(secondary?.textContent).toContain('mroot');
+ });
+
+ it('should render site as SecondaryHint inside Secondary when site is provided', () => {
+ render();
+ expect(screen.getByText('(corp)', { exact: false })).toBeInTheDocument();
+ });
+
+ it('should render login in Primary when firstName is missing', () => {
+ render();
+ const primary = document.querySelector('[class*="primary"]');
+ expect(primary?.textContent).toContain('mroot');
+ });
+
+ it('should render login in Primary when lastName is missing', () => {
+ render();
+ const primary = document.querySelector('[class*="primary"]');
+ expect(primary?.textContent).toContain('mroot');
+ });
+
+ it('should render login as primary content when full name is absent', () => {
+ render();
+ const primary = document.querySelector('[class*="primary"]');
+ expect(primary?.textContent).toContain('mroot');
+ });
+ });
+
+ describe('compact mode', () => {
+ it('should render a single Primary containing the formatted name', () => {
+ render();
+ const primary = document.querySelector('[class*="primary"]');
+ expect(primary?.textContent).toContain('Root M. A.');
+ });
+
+ it('should render login inside Primary in compact mode when full name is absent', () => {
+ render();
+ const primary = document.querySelector('[class*="primary"]');
+ expect(primary?.textContent).toContain('mroot');
+ });
+
+ it('should render site as SecondaryHint inside Primary in compact mode', () => {
+ render();
+ const primary = document.querySelector('[class*="primary"]');
+ expect(primary?.textContent).toContain('(corp)');
+ });
+
+ it('should not render site when neither full name nor login is available in compact mode', () => {
+ render();
+ expect(screen.queryByText('(corp)', { exact: false })).toBeNull();
+ });
+ });
+
+ describe('formatter prop', () => {
+ it('should use the provided formatter instead of formatUsername', () => {
+ const customFormatter = vi.fn().mockReturnValue('Custom Name');
+ render();
+ render();
+ expect(customFormatter).toHaveBeenCalledWith(fullProfile, 'lf.m.');
+ expect(screen.getByText('Custom Name')).toBeInTheDocument();
+ });
+
+ it('should pass fullNameFormat to the formatter', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Maxwell Root')).toBeInTheDocument();
+ });
+ });
+
+ describe('custom view (children)', () => {
+ it('should render children instead of the default template', () => {
+ render(
+
+ Custom Name
+
+ );
+
+ expect(screen.getByText('Custom Name')).toBeInTheDocument();
+ });
+
+ it('should not render auto-generated content when children are provided', () => {
+ render(
+
+ Custom
+
+ );
+
+ expect(screen.queryByText('Root M. A.')).toBeNull();
+ expect(screen.queryByText('mroot')).toBeNull();
+ });
+
+ it('should still apply data attributes to the root span in custom view', () => {
+ render(
+
+ Custom
+
+ );
+
+ expect(getRoot()).toHaveAttribute('data-mode', 'stacked');
+ expect(getRoot()).toHaveAttribute('data-type', 'error');
+ });
+ });
+});
+
+describe('formatUsername', () => {
+ it('should return an empty string when userInfo is undefined', () => {
+ expect(formatUsername(undefined)).toBe('');
+ });
+
+ it('should format "lf.m." correctly with all three name parts present', () => {
+ expect(
+ formatUsername(
+ { firstName: 'Maxwell', middleName: 'Alan', lastName: 'Root' },
+ 'lf.m.'
+ )
+ ).toBe('Root M. A.');
+ });
+
+ it('should skip empty fields without leaving stray dots', () => {
+ expect(
+ formatUsername({ firstName: 'Maxwell', lastName: 'Root' }, 'lf.m.')
+ ).toBe('Root M.');
+ });
+
+ it('should emit the full field value when the next character is not a dot', () => {
+ expect(
+ formatUsername({ firstName: 'Maxwell', lastName: 'Root' }, 'lf')
+ ).toBe('Root Maxwell');
+ });
+
+ it('should handle a custom format string', () => {
+ expect(
+ formatUsername({ firstName: 'Maxwell', lastName: 'Root' }, 'fl.')
+ ).toBe('Maxwell R.');
+ });
+});
+
+describe('formatUsernameCustom', () => {
+ it('should return an empty string when userInfo is undefined', () => {
+ expect(formatUsernameCustom(undefined)).toBe('');
+ });
+
+ it('should format "L f. m." correctly with all three name parts present', () => {
+ expect(
+ formatUsernameCustom(
+ { firstName: 'Maxwell', middleName: 'Alan', lastName: 'Root' },
+ 'L f. m.'
+ )
+ ).toBe('Root M. A.');
+ });
+
+ it('should emit the full value for uppercase keys', () => {
+ expect(
+ formatUsernameCustom({ firstName: 'Maxwell', lastName: 'Root' }, 'F L')
+ ).toBe('Maxwell Root');
+ });
+
+ it('should emit only the initial for lowercase keys', () => {
+ expect(
+ formatUsernameCustom({ firstName: 'Maxwell', lastName: 'Root' }, 'f l')
+ ).toBe('M R');
+ });
+
+ it('should emit non-field characters literally', () => {
+ expect(
+ formatUsernameCustom({ firstName: 'Maxwell', lastName: 'Root' }, 'L, F')
+ ).toBe('Root, Maxwell');
+ });
+
+ it('should silently drop an empty field key while preserving surrounding literals', () => {
+ expect(
+ formatUsernameCustom(
+ { firstName: 'Maxwell', lastName: 'Root' },
+ 'L f. m.'
+ )
+ ).toBe('Root M. .');
+ });
+
+ it('should accept a custom mapping', () => {
+ expect(
+ formatUsernameCustom({ firstName: 'Maxwell', lastName: 'Root' }, 'A', {
+ A: 'firstName',
+ })
+ ).toBe('Maxwell');
+ });
+});
+
+describe('buildUsernameText', () => {
+ it('should return only the name when login and site are absent', () => {
+ expect(buildUsernameText({ name: 'Root M. A.' })).toBe('Root M. A.');
+ });
+
+ it('should append login after the name', () => {
+ expect(buildUsernameText({ name: 'Root M. A.', login: 'mroot' })).toBe(
+ 'Root M. A. mroot'
+ );
+ });
+
+ it('should wrap site in parentheses by default', () => {
+ expect(
+ buildUsernameText({ name: 'Root M. A.', login: 'mroot', site: 'corp' })
+ ).toBe('Root M. A. mroot (corp)');
+ });
+
+ it('should apply a custom formatSite', () => {
+ expect(
+ buildUsernameText(
+ { name: 'Root M. A.', login: 'mroot', site: 'corp' },
+ { formatSite: (s) => s }
+ )
+ ).toBe('Root M. A. mroot corp');
+ });
+
+ it('should apply a custom formatLogin', () => {
+ expect(
+ buildUsernameText(
+ { name: 'Root M. A.', login: 'mroot', site: 'corp' },
+ { formatLogin: (l) => `[${l}]` }
+ )
+ ).toBe('Root M. A. [mroot] (corp)');
+ });
+
+ it('should omit login segment when login is absent', () => {
+ expect(buildUsernameText({ name: 'Root M. A.', site: 'corp' })).toBe(
+ 'Root M. A. (corp)'
+ );
+ });
+
+ it('should omit site segment when site is absent', () => {
+ expect(buildUsernameText({ name: 'Root M. A.', login: 'mroot' })).toBe(
+ 'Root M. A. mroot'
+ );
+ });
+});
diff --git a/packages/components/src/components/Username/Username.tsx b/packages/components/src/components/Username/Username.tsx
new file mode 100644
index 00000000..0e00065d
--- /dev/null
+++ b/packages/components/src/components/Username/Username.tsx
@@ -0,0 +1,109 @@
+'use client';
+
+import {
+ forwardRef,
+ type ComponentPropsWithRef,
+ type ComponentRef,
+} from 'react';
+
+import { clsx, isNotNil } from '@koobiq/react-core';
+
+import type { UsernameBaseProps } from './types';
+import s from './Username.module.css';
+import { UsernamePrimary } from './UsernamePrimary';
+import { UsernameSecondary } from './UsernameSecondary';
+import { UsernameSecondaryHint } from './UsernameSecondaryHint';
+import { formatUsername } from './utils';
+
+/**
+ * Displays a user's name based on profile data.
+ * Supports different layout modes and visual styles.
+ * Provide `children` to take full control of the rendered content.
+ */
+export const Username = forwardRef, UsernameBaseProps>(
+ (props, ref) => {
+ const {
+ userInfo,
+ mode = 'inline',
+ type = 'default',
+ isCompact = false,
+ fullNameFormat = 'lf.m.',
+ formatter = formatUsername,
+ children,
+ className,
+ ...other
+ } = props;
+
+ const rootClassName = clsx(s.base, s[mode], s[type], className);
+
+ const rootProps = {
+ 'data-mode': mode,
+ 'data-type': type,
+ className: rootClassName,
+ ref,
+ ...other,
+ };
+
+ // When children are provided, render as a custom view (overrides default template).
+ if (isNotNil(children)) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ const hasFullName = Boolean(userInfo?.firstName && userInfo?.lastName);
+ const name = hasFullName ? formatter(userInfo, fullNameFormat) : '';
+ const hasContent = hasFullName || isNotNil(userInfo?.login);
+
+ if (isCompact) {
+ return (
+
+
+ {hasFullName ? name : userInfo?.login}
+ {hasContent && isNotNil(userInfo?.site) && (
+ ({userInfo!.site})
+ )}
+
+
+ );
+ }
+
+ if (hasFullName) {
+ return (
+
+ {name}
+ {isNotNil(userInfo?.login) && (
+
+ {userInfo!.login}
+ {isNotNil(userInfo?.site) && (
+
+ {' '}
+ ({userInfo!.site})
+
+ )}
+
+ )}
+
+ );
+ }
+
+ return (
+
+ {isNotNil(userInfo?.login) && (
+
+ {userInfo!.login}
+ {isNotNil(userInfo?.site) && (
+ ({userInfo!.site})
+ )}
+
+ )}
+
+ );
+ }
+);
+
+Username.displayName = 'Username';
+
+export type UsernameProps = ComponentPropsWithRef;
diff --git a/packages/components/src/components/Username/UsernamePrimary.tsx b/packages/components/src/components/Username/UsernamePrimary.tsx
new file mode 100644
index 00000000..48c29294
--- /dev/null
+++ b/packages/components/src/components/Username/UsernamePrimary.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { forwardRef, type ComponentRef } from 'react';
+
+import { clsx } from '@koobiq/react-core';
+
+import type { UsernamePrimaryProps } from './types';
+import s from './Username.module.css';
+
+export const UsernamePrimary = forwardRef<
+ ComponentRef<'span'>,
+ UsernamePrimaryProps
+>(({ className, children, ...other }, ref) => (
+
+ {children}
+
+));
+
+UsernamePrimary.displayName = 'UsernamePrimary';
diff --git a/packages/components/src/components/Username/UsernameSecondary.tsx b/packages/components/src/components/Username/UsernameSecondary.tsx
new file mode 100644
index 00000000..fbbd3cde
--- /dev/null
+++ b/packages/components/src/components/Username/UsernameSecondary.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { forwardRef, type ComponentRef } from 'react';
+
+import { clsx } from '@koobiq/react-core';
+
+import type { UsernameSecondaryProps } from './types';
+import s from './Username.module.css';
+
+export const UsernameSecondary = forwardRef<
+ ComponentRef<'span'>,
+ UsernameSecondaryProps
+>(({ className, children, ...other }, ref) => (
+
+ {children}
+
+));
+
+UsernameSecondary.displayName = 'UsernameSecondary';
diff --git a/packages/components/src/components/Username/UsernameSecondaryHint.tsx b/packages/components/src/components/Username/UsernameSecondaryHint.tsx
new file mode 100644
index 00000000..4a3ce249
--- /dev/null
+++ b/packages/components/src/components/Username/UsernameSecondaryHint.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { forwardRef, type ComponentRef } from 'react';
+
+import { clsx } from '@koobiq/react-core';
+
+import type { UsernameSecondaryHintProps } from './types';
+import s from './Username.module.css';
+
+export const UsernameSecondaryHint = forwardRef<
+ ComponentRef<'span'>,
+ UsernameSecondaryHintProps
+>(({ className, children, ...other }, ref) => (
+
+ {children}
+
+));
+
+UsernameSecondaryHint.displayName = 'UsernameSecondaryHint';
diff --git a/packages/components/src/components/Username/index.ts b/packages/components/src/components/Username/index.ts
new file mode 100644
index 00000000..174cb12e
--- /dev/null
+++ b/packages/components/src/components/Username/index.ts
@@ -0,0 +1,6 @@
+export * from './Username';
+export * from './UsernamePrimary';
+export * from './UsernameSecondary';
+export * from './UsernameSecondaryHint';
+export * from './types';
+export * from './utils';
diff --git a/packages/components/src/components/Username/types.ts b/packages/components/src/components/Username/types.ts
new file mode 100644
index 00000000..d9809bc4
--- /dev/null
+++ b/packages/components/src/components/Username/types.ts
@@ -0,0 +1,64 @@
+import type { ComponentPropsWithRef, ReactNode } from 'react';
+
+export const usernamePropMode = ['stacked', 'inline', 'text'] as const;
+export type UsernamePropMode = (typeof usernamePropMode)[number];
+
+export const usernamePropType = [
+ 'default',
+ 'error',
+ 'accented',
+ 'inherit',
+] as const;
+export type UsernamePropType = (typeof usernamePropType)[number];
+
+export type UsernameUserInfo = {
+ firstName?: string;
+ lastName?: string;
+ middleName?: string;
+ login?: string;
+ site?: string;
+};
+
+export type UsernameBaseProps = Omit<
+ ComponentPropsWithRef<'span'>,
+ 'children'
+> & {
+ /** User profile data. Not required when children (custom view) is provided. */
+ userInfo?: UsernameUserInfo;
+ /**
+ * Layout mode.
+ * @default 'inline'
+ */
+ mode?: UsernamePropMode;
+ /**
+ * Visual color type.
+ * @default 'default'
+ */
+ type?: UsernamePropType;
+ /**
+ * When true, collapses primary and secondary into a single line.
+ * @default false
+ */
+ isCompact?: boolean;
+ /**
+ * Format string passed to `formatter`.
+ * Interpretation depends on which formatter is used.
+ * @default 'lf.m.'
+ */
+ fullNameFormat?: string;
+ /**
+ * Function used to format the full name from `userInfo`.
+ * Accepts `formatUsername` (default) or `formatUsernameCustom`.
+ * Signature: `(userInfo, format) => string`
+ */
+ formatter?: (
+ userInfo: UsernameUserInfo | undefined,
+ format: string
+ ) => string;
+ /** When provided, entirely replaces the auto-rendered template. */
+ children?: ReactNode;
+};
+
+export type UsernamePrimaryProps = ComponentPropsWithRef<'span'>;
+export type UsernameSecondaryProps = ComponentPropsWithRef<'span'>;
+export type UsernameSecondaryHintProps = ComponentPropsWithRef<'span'>;
diff --git a/packages/components/src/components/Username/utils.ts b/packages/components/src/components/Username/utils.ts
new file mode 100644
index 00000000..d65f8d75
--- /dev/null
+++ b/packages/components/src/components/Username/utils.ts
@@ -0,0 +1,160 @@
+import type { UsernameUserInfo } from './types';
+
+const fieldMap: Record = {
+ l: 'lastName',
+ f: 'firstName',
+ m: 'middleName',
+};
+
+/**
+ * Maps format-string characters to `UsernameUserInfo` field names.
+ * Used by `formatUsernameCustom`. Characters absent from the mapping
+ * (or mapped to `undefined`) are emitted as literals.
+ */
+export type UsernameFormatMapping = Record<
+ string,
+ keyof UsernameUserInfo | undefined
+>;
+
+const defaultCustomMapping: UsernameFormatMapping = {
+ F: 'firstName',
+ f: 'firstName',
+ M: 'middleName',
+ m: 'middleName',
+ L: 'lastName',
+ l: 'lastName',
+};
+
+/**
+ * Formats user profile data into a display name string using a format pattern.
+ *
+ * Format characters: `l` = lastName, `f` = firstName, `m` = middleName.
+ * A letter followed by `.` produces an initial + period (e.g. `f.` → "M.").
+ * Empty fields are silently skipped without leaving stray punctuation.
+ * @example
+ * formatUsername({ firstName: 'Maxwell', middleName: 'Alan', lastName: 'Root' }, 'lf.m.')
+ * // → "Root M. A."
+ */
+export function formatUsername(
+ userInfo: UsernameUserInfo | undefined,
+ format = 'lf.m.'
+): string {
+ if (!userInfo) return '';
+
+ const tokens: string[] = [];
+ let i = 0;
+
+ while (i < format.length) {
+ const char = format[i];
+ const fieldKey = fieldMap[char];
+
+ if (fieldKey) {
+ const value = userInfo[fieldKey]?.trim() ?? '';
+ const takesInitial = format[i + 1] === '.';
+
+ if (value) {
+ tokens.push(takesInitial ? `${value[0].toUpperCase()}.` : value);
+ }
+
+ // consume the trailing '.' whether the field had a value
+ if (takesInitial) i += 1;
+ }
+
+ // standalone '.' is only a separator marker — never emitted literally
+ i += 1;
+ }
+
+ return tokens.join(' ');
+}
+
+/**
+ * Formats user profile data into a display name string using a richer format pattern.
+ *
+ * - Uppercase letters (`L`, `F`, `M`) → full field value.
+ * - Lowercase letters (`l`, `f`, `m`) → first character (initial) only.
+ * - All other characters are emitted literally (spaces, dots, slashes, etc.).
+ * - Empty fields are silently dropped; surrounding literal characters still emit.
+ *
+ * Default format `'L f. m.'` produces `"Root M. A."` for
+ * `{ firstName: 'Maxwell', middleName: 'Alan', lastName: 'Root' }`.
+ *
+ * Pass a custom `mapping` to bind format keys to different `UsernameUserInfo` fields.
+ * @example
+ * formatUsernameCustom({ firstName: 'Maxwell', middleName: 'Alan', lastName: 'Root' }, 'L f. m.')
+ * // → "Root M. A."
+ */
+export function formatUsernameCustom(
+ userInfo: UsernameUserInfo | undefined,
+ format = 'L f. m.',
+ mapping: UsernameFormatMapping = defaultCustomMapping
+): string {
+ if (!userInfo) return '';
+
+ let result = '';
+
+ for (const char of format) {
+ if (!(char in mapping)) {
+ result += char;
+ continue;
+ }
+
+ const fieldKey = mapping[char];
+
+ if (!fieldKey) {
+ result += char;
+ continue;
+ }
+
+ const value = userInfo[fieldKey]?.trim() ?? '';
+
+ if (value) {
+ result += char === char.toLowerCase() ? value[0] : value;
+ }
+ }
+
+ return result.trim();
+}
+
+export type BuildUsernameTextOptions = {
+ /**
+ * Formats the login segment.
+ * @default (login) => login
+ */
+ formatLogin?: (login: string) => string;
+ /**
+ * Formats the site segment.
+ * @default (site) => `(${site})`
+ */
+ formatSite?: (site: string) => string;
+};
+
+/**
+ * Builds a plain-text string from a pre-formatted name plus optional login and site,
+ * mirroring the text rendered by `Username`.
+ *
+ * Pass the result of `formatUsername` or `formatUsernameCustom` as `name`.
+ * Use `formatLogin`/`formatSite` options to customise how those segments appear.
+ * @example
+ * const name = formatUsername(userInfo, 'lf.m.');
+ * buildUsernameText({ name, login: userInfo.login, site: userInfo.site });
+ * // → "Root M. A. mroot (corp)"
+ * @example
+ * // Strip parentheses from site (useful for search/filter comparisons)
+ * buildUsernameText({ name, login, site }, { formatSite: (s) => s });
+ * // → "Root M. A. mroot corp"
+ */
+export function buildUsernameText(
+ data: { name: string; login?: string; site?: string },
+ options?: BuildUsernameTextOptions
+): string {
+ const formatLogin = options?.formatLogin ?? ((login) => login);
+ const formatSite = options?.formatSite ?? ((site) => `(${site})`);
+
+ return [
+ data.name,
+ data.login && formatLogin(data.login),
+ data.site && formatSite(data.site),
+ ]
+ .filter(Boolean)
+ .join(' ');
+}
diff --git a/packages/components/src/components/index.ts b/packages/components/src/components/index.ts
index 9837688b..1a346b44 100644
--- a/packages/components/src/components/index.ts
+++ b/packages/components/src/components/index.ts
@@ -56,6 +56,7 @@ export * from './TreeSelect';
export * from './EmptyState';
export * from './Flag';
export * from './Sidebar';
+export * from './Username';
export * from './layout';
export {
useListData,
diff --git a/tools/api-extractor/config.json b/tools/api-extractor/config.json
index 205f5c09..5c0e2cb4 100644
--- a/tools/api-extractor/config.json
+++ b/tools/api-extractor/config.json
@@ -57,7 +57,8 @@
"Tooltip",
"Tree",
"TreeSelect",
- "Typography"
+ "Typography",
+ "Username"
],
"packages": [
{ "dir": "primitives", "report": "react-primitives" },
diff --git a/tools/public_api_guard/components/Username.api.md b/tools/public_api_guard/components/Username.api.md
new file mode 100644
index 00000000..bb667b30
--- /dev/null
+++ b/tools/public_api_guard/components/Username.api.md
@@ -0,0 +1,94 @@
+## API Report File for "koobiq-react"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { ComponentPropsWithRef } from 'react';
+import { DetailedHTMLProps } from 'react';
+import { ForwardRefExoticComponent } from 'react';
+import { HTMLAttributes } from 'react';
+import type { ReactNode } from 'react';
+import { RefAttributes } from 'react';
+
+// @public
+export function buildUsernameText(data: {
+ name: string;
+ login?: string;
+ site?: string;
+}, options?: BuildUsernameTextOptions): string;
+
+// @public (undocumented)
+export type BuildUsernameTextOptions = {
+ formatLogin?: (login: string) => string;
+ formatSite?: (site: string) => string;
+};
+
+// @public
+export function formatUsername(userInfo: UsernameUserInfo | undefined, format?: string): string;
+
+// @public
+export function formatUsernameCustom(userInfo: UsernameUserInfo | undefined, format?: string, mapping?: UsernameFormatMapping): string;
+
+// @public
+export const Username: ForwardRefExoticComponent & RefAttributes>;
+
+// @public (undocumented)
+export type UsernameBaseProps = Omit, 'children'> & {
+ userInfo?: UsernameUserInfo;
+ mode?: UsernamePropMode;
+ type?: UsernamePropType;
+ isCompact?: boolean;
+ fullNameFormat?: string;
+ formatter?: (userInfo: UsernameUserInfo | undefined, format: string) => string;
+ children?: ReactNode;
+};
+
+// @public
+export type UsernameFormatMapping = Record;
+
+// @public (undocumented)
+export const UsernamePrimary: ForwardRefExoticComponent, HTMLSpanElement>, "ref"> & RefAttributes>;
+
+// @public (undocumented)
+export type UsernamePrimaryProps = ComponentPropsWithRef<'span'>;
+
+// @public (undocumented)
+export type UsernamePropMode = (typeof usernamePropMode)[number];
+
+// @public (undocumented)
+export const usernamePropMode: readonly ["stacked", "inline", "text"];
+
+// @public (undocumented)
+export type UsernameProps = ComponentPropsWithRef;
+
+// @public (undocumented)
+export type UsernamePropType = (typeof usernamePropType)[number];
+
+// @public (undocumented)
+export const usernamePropType: readonly ["default", "error", "accented", "inherit"];
+
+// @public (undocumented)
+export const UsernameSecondary: ForwardRefExoticComponent, HTMLSpanElement>, "ref"> & RefAttributes>;
+
+// @public (undocumented)
+export const UsernameSecondaryHint: ForwardRefExoticComponent, HTMLSpanElement>, "ref"> & RefAttributes>;
+
+// @public (undocumented)
+export type UsernameSecondaryHintProps = ComponentPropsWithRef<'span'>;
+
+// @public (undocumented)
+export type UsernameSecondaryProps = ComponentPropsWithRef<'span'>;
+
+// @public (undocumented)
+export type UsernameUserInfo = {
+ firstName?: string;
+ lastName?: string;
+ middleName?: string;
+ login?: string;
+ site?: string;
+};
+
+// (No @packageDocumentation comment for this package)
+
+```