Skip to content
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"bumpp": "^10.1.0",
"chalk": "^4.1.2",
"commit-and-tag-version": "^12.5.0",
"country-flag-icons": "^1.6.20",
"dotenv": "^16.4.7",
"eslint": "^9.39.4",
"eslint-config-prettier": "^9.1.2",
Expand Down
130 changes: 130 additions & 0 deletions packages/components/src/components/Flag/Flag.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
Meta,
Story,
Props,
Status,
} from '../../../../../.storybook/components';

import * as Stories from './Flag.stories';

<Meta of={Stories} />

# Flag

<Status variant="experimental" />

Flag shows a small country flag inside a product UI (inline with text, in lists, options, selects).

It is a thin presentational wrapper: it decorates a flag graphic you provide and does **not** ship
or resolve any flag data. Flag graphics are expected to come from
[`country-flag-icons`](https://www.npmjs.com/package/country-flag-icons) (full ISO 3166-1, redrawn
for small sizes) or any custom source.

## Import

```tsx
import { Flag } from '@koobiq/react-components';
```

## Usage

Provide the flag graphic as `children`. The recommended path is a ready-made React flag component
from [`country-flag-icons`](https://www.npmjs.com/package/country-flag-icons) — no extra requests, no
base64, and it renders an inline `<svg>` under the hood. A raw inline `<svg>` or an `<img>` also
works.

```tsx
import { DE } from 'country-flag-icons/react/3x2';

<Flag role="img" aria-label="Germany">
<DE />
</Flag>;
```

<Story of={Stories.Base} />

## Props

<Props of={Stories.Base} />

## Shape

The `shape` prop controls the corner treatment: `rectangle` (default) or `circle`. `circle` clips the
flag to a circle and defaults its ratio to `1 / 1`. For a sharp-cornered square, use
`aspectRatio="1 / 1"` (see below).

<Story of={Stories.Shape} />

## Shadow

A thin inset hairline separates the flag from the background. It is on by default and adapts to the theme (dark hairline in the light theme, lighter in the dark theme).

<Story of={Stories.Shadow} />

## Sizing
Comment thread
KamilEmeleev marked this conversation as resolved.

By default the flag's height is `1em`, so it tracks the surrounding text — inline, in lists, and in
options you set nothing. For an explicit height use the `size` prop (a number is pixels, a string is
any CSS length), or override the `--kbq-flag-size` variable to size a whole context at once.

```tsx
<Flag size={24} /> // 24px
<Flag size="2rem" /> // any CSS length
```

<Story of={Stories.Sizes} />

## Aspect ratio

Set the box ratio with the `aspectRatio` prop.

<Story of={Stories.AspectRatio} />

## Missing flag

The library ships no flag data, so guard the country code (e.g. with `hasFlag` from
`country-flag-icons`) and render a neutral placeholder when a flag is missing —
the layout never breaks and no broken graphic is shown.

<Story of={Stories.Fallback} />

## Accessibility

A flag must never be a silent, meaning-bearing graphic. Flag adds no ARIA semantics on its own —
supply them with standard attributes:

- When the flag carries meaning and has **no adjacent text**, pass a `aria-label` and `role="img"`.
- When adjacent visible text already names the country, mark the flag with `aria-hidden="true"`. It is hidden from
assistive technology to avoid duplication.

<Story of={Stories.Accessibility} />

## Flag is not a language

A flag represents a country, not a language. Use a globe icon (not a flag) for language selection.

<Story of={Stories.NotForLanguage} />

## Custom look

Stylized, decorative treatments (rounded corners, drop shadow, "folds" gradient) are your own CSS
plus the exposed variables — not a library prop. Round the corners with `--kbq-flag-border-radius`
and add a shadow/gradient in your own styles.

<Story of={Stories.Custom} />

## Flags in a Select

<Story of={Stories.FlagsInSelect} />

## CSS variables

Everything visual that isn't a discrete state is an overridable CSS variable:

| Variable | Default | Purpose |
| ----------------------------- | --------------------------------------- | ----------------------------------------------------- |
| `--kbq-flag-size` | `1em` | Flag height (also settable via the `size` prop). |
| `--kbq-flag-aspect-ratio` | `3 / 2` | Box ratio (also settable via the `aspectRatio` prop). |
| `--kbq-flag-border-radius` | `0` | Corner radius (rounded / stylized look). |
| `--kbq-flag-shadow-color` | `var(--kbq-line-contrast-fade)` | Inset hairline color; theme-adaptive. |
| `--kbq-flag-empty-background` | `var(--kbq-states-background-disabled)` | Placeholder fill. |
47 changes: 47 additions & 0 deletions packages/components/src/components/Flag/Flag.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.base {
/* Public, overridable knobs (see the component docs). */
--kbq-flag-aspect-ratio: 3 / 2;
Comment thread
KamilEmeleev marked this conversation as resolved.
--kbq-flag-border-radius: 0;
--kbq-flag-shadow-color: var(--kbq-line-contrast-fade);
--kbq-flag-empty-background: var(--kbq-states-background-disabled);

display: inline-block;
position: relative;
overflow: hidden;
vertical-align: middle;
block-size: var(--kbq-flag-size, 1em);
aspect-ratio: var(--kbq-flag-aspect-ratio);
border-radius: var(--kbq-flag-border-radius);
}

/* The projected flag graphic fills the box (descendant selector supports wrapped graphics). */
.base :is(svg, img) {
display: block;
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}

/* Inset hairline separating the flag from the background (shown by default). */
.base::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
border: 1px solid var(--kbq-flag-shadow-color);
pointer-events: none;
}

.base[data-hide-shadow]::after {
content: none;
}

/* `circle` only clips the corners; its 1:1 ratio comes from the component. */
.base[data-shape='circle'] {
border-radius: 50%;
}

/* No projected graphic → neutral placeholder (e.g. unknown/invalid country). */
.base:empty {
background-color: var(--kbq-flag-empty-background);
}
21 changes: 21 additions & 0 deletions packages/components/src/components/Flag/Flag.stories.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* Gradient imitating folds, blended over the flag (stylized docs example). */
.stylized::before {
content: '';
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
background: linear-gradient(
240.64deg,
rgb(255 255 255 / 30%) 0%,
rgb(0 0 0 / 27%) 26.27%,
rgb(255 255 255 / 26%) 37%,
rgb(0 0 0 / 55%) 48.7%,
rgb(0 0 0 / 24%) 59.44%,
rgb(255 255 255 / 30%) 73.64%,
rgb(39 39 39 / 22%) 90.15%,
rgb(0 0 0 / 20%) 100%
);
mix-blend-mode: overlay;
pointer-events: none;
}
Loading
Loading