Skip to content
Draft
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
122 changes: 122 additions & 0 deletions packages/design-system/docusaurus/docs/interaction/Select.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
id: select
title: Select
sidebar_label: Select
description: Native select dropdown for form inputs with design system styling.
sidebar_position: 9
---

import { Label, Select } from '@grasdouble/lufa_design-system';

import { DarkModeCompatible } from '../../src/components/DarkModeCompatible';
import { LiveDemoSection } from '../../src/components/LiveDemoSection';
import { DisabledDemo, ErrorDemo, FullWidthDemo, LiveDemo, SizesDemo } from '../../src/dsExamples/interaction/select';

# Select

<DarkModeCompatible />

## Overview

**Select** is a native dropdown component for form inputs. It matches the visual design of **Input** and supports placeholder text, error messages, disabled state, full width layout, and three size variants.

### Live Demo

<LiveDemoSection
tabs={[
{ id: 'default', label: 'Default', content: <LiveDemo /> },
{ id: 'error', label: 'Error', content: <ErrorDemo /> },
{ id: 'disabled', label: 'Disabled', content: <DisabledDemo /> },
{ id: 'full', label: 'Full width', content: <FullWidthDemo /> },
{ id: 'sizes', label: 'Sizes', content: <SizesDemo /> },
]}
/>

## Anatomy

- **Root**: A `<div>` wrapper containing the `<select>` element and optional error message.
- **Select**: The native `<select>` element with design token styling.
- **Option**: The `Select.Option` sub-component, which renders a native `<option>` element.
- **Error message**: An accessible `<span>` shown below the select when `error` is provided.

## Usage

```tsx
import { Label, Select } from '@grasdouble/lufa_design-system';

export function Example() {
const [value, setValue] = React.useState('');

return (
<>
<Label htmlFor="model">AI Model</Label>
<Select
id="model"
value={value}
onChange={setValue}
placeholder="Select a model"
defaultValue=""
>
<Select.Option value="gpt-4">GPT-4</Select.Option>
<Select.Option value="gpt-3.5">GPT-3.5 Turbo</Select.Option>
<Select.Option value="claude-3">Claude 3</Select.Option>
</Select>
</>
);
}
```

## Props

### Select

| Prop | Type | Default | Description |
| ------------- | --------------------------- | ----------- | ------------------------------------------------------------------ |
| `onChange` | `(value: string) => void` | `undefined` | Callback fired when the selected value changes. |
| `placeholder` | `string` | `undefined` | Placeholder text shown as a disabled empty option. |
| `error` | `string` | `undefined` | Error message. Applies error styling and displays the message. |
| `disabled` | `boolean` | `false` | Disables interaction and applies disabled styling. |
| `fullWidth` | `boolean` | `false` | Stretches the select to 100% of container width. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size variant. |
| `className` | `string` | `undefined` | Additional CSS classes applied to the select element. |
| `...props` | `SelectHTMLAttributes` | - | All standard select attributes (value, defaultValue, name, etc.). |

### Select.Option

| Prop | Type | Default | Description |
| ---------- | -------- | ----------- | ------------------------------------------ |
| `value` | `string` | - | The option value. |
| `...props` | `OptionHTMLAttributes` | - | All standard option attributes. |

## Accessibility

- Always associate a `<Label>` with the select using `htmlFor` and `id`.
- When `error` is provided, the select receives `aria-invalid="true"` and `aria-describedby` pointing to the error message element.
- Keyboard navigation (arrow keys, Enter, Escape) is handled natively by the browser.

## Theming & Tokens

Select reuses the Input component tokens for visual consistency. These tokens are theme‑aware.

| Token | Usage |
| ------------------------------------------- | ------------------ |
| `--lufa-component-input-background-default` | Background |
| `--lufa-component-input-border-default` | Border |
| `--lufa-component-input-border-focus` | Focus border |
| `--lufa-component-input-border-error` | Error border |
| `--lufa-component-input-text-default` | Text color |

## Do / Don't

:::tip Do
Use **Select** with **Label** for accessible form fields. Show an error message when validation fails.
:::

:::warning Don't
Avoid using **Select** for very long option lists—consider a searchable autocomplete instead.
:::

## Related Components

- [Input](/docs/interaction/input)
- [Label](/docs/interaction/label)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Live examples for Select component documentation
*/

import React, { useState } from 'react';

import { Label, Select } from '@grasdouble/lufa_design-system';

export function LiveDemo() {
const [value, setValue] = useState('');

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Label htmlFor="demo-model">AI Model</Label>
<Select id="demo-model" value={value} onChange={setValue} placeholder="Select a model">
<Select.Option value="gpt-4">GPT-4</Select.Option>
<Select.Option value="gpt-3.5">GPT-3.5 Turbo</Select.Option>
<Select.Option value="claude-3">Claude 3</Select.Option>
<Select.Option value="mistral">Mistral</Select.Option>
</Select>
{value && <span style={{ fontSize: '14px' }}>Selected: {value}</span>}
</div>
);
}

export function DisabledDemo() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<Label htmlFor="demo-disabled">Disabled</Label>
<Select id="demo-disabled" disabled>
<Select.Option value="option1">Option 1</Select.Option>
</Select>
</div>
</div>
);
}

export function FullWidthDemo() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<Label htmlFor="demo-full">Full width</Label>
<Select id="demo-full" fullWidth>
<Select.Option value="option1">Option 1</Select.Option>
<Select.Option value="option2">Option 2</Select.Option>
</Select>
</div>
);
}

export function ErrorDemo() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<Label htmlFor="demo-error">Category</Label>
<Select id="demo-error" error="Please select a category">
<Select.Option value="a">Category A</Select.Option>
<Select.Option value="b">Category B</Select.Option>
</Select>
</div>
);
}

export function SizesDemo() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div>
<Label htmlFor="demo-sm">Small (sm)</Label>
<Select id="demo-sm" size="sm">
<Select.Option value="a">Option A</Select.Option>
<Select.Option value="b">Option B</Select.Option>
</Select>
</div>
<div>
<Label htmlFor="demo-md">Medium (md)</Label>
<Select id="demo-md" size="md">
<Select.Option value="a">Option A</Select.Option>
<Select.Option value="b">Option B</Select.Option>
</Select>
</div>
<div>
<Label htmlFor="demo-lg">Large (lg)</Label>
<Select id="demo-lg" size="lg">
<Select.Option value="a">Option A</Select.Option>
<Select.Option value="b">Option B</Select.Option>
</Select>
</div>
</div>
);
}
122 changes: 122 additions & 0 deletions packages/design-system/main/src/interaction/Select/Select.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Select Component - CSS Module
*
* Uses component input tokens for visual consistency with the Input component,
* and semantic tokens for size variants (matching Button sizing).
*/

/* ========================================== */
/* WRAPPER */
/* ========================================== */

/* Wrapper to support error message display and full-width layout */
.wrapper {
display: inline-flex;
flex-direction: column;
gap: var(--lufa-component-input-label-spacing);
}

.wrapper.fullWidth {
display: flex;
width: 100%;
}

/* ========================================== */
/* BASE CLASS */
/* ========================================== */

/* Base Select class */
.select {
display: inline-block;
box-sizing: border-box;
width: 100%;
padding-block: var(--lufa-component-input-padding-md-block);
padding-inline: var(--lufa-component-input-padding-md-inline);
font-family: inherit;
font-size: var(--lufa-component-input-font-size-md);
line-height: var(--lufa-core-typography-body-line-height);
color: var(--lufa-component-input-text-default);
background-color: var(--lufa-component-input-background-default);
border: var(--lufa-component-input-border-width) solid var(--lufa-component-input-border-default);
border-radius: var(--lufa-component-input-border-radius);
transition:
border-color var(--lufa-semantic-ui-transition-duration-fast),
box-shadow var(--lufa-semantic-ui-transition-duration-fast);
outline: none;
cursor: pointer;
appearance: auto;
}

/* ========================================== */
/* SIZE */
/* ========================================== */

.size-sm {
padding-block: var(--lufa-component-input-padding-sm-block);
padding-inline: var(--lufa-component-input-padding-sm-inline);
font-size: var(--lufa-component-input-font-size-sm);
}

.size-md {
padding-block: var(--lufa-component-input-padding-md-block);
padding-inline: var(--lufa-component-input-padding-md-inline);
font-size: var(--lufa-component-input-font-size-md);
}

.size-lg {
padding-block: var(--lufa-component-input-padding-lg-block);
padding-inline: var(--lufa-component-input-padding-lg-inline);
font-size: var(--lufa-component-input-font-size-lg);
}

/* ========================================== */
/* ERROR */
/* ========================================== */

.error {
border-color: var(--lufa-component-input-border-error);
}

/* ========================================== */
/* DISABLED */
/* ========================================== */

.disabled {
background-color: var(--lufa-component-input-background-disabled);
color: var(--lufa-component-input-text-disabled);
border-color: var(--lufa-component-input-border-disabled);
cursor: var(--lufa-component-input-state-disabled-cursor);
}

/* ========================================== */
/* FULLWIDTH */
/* ========================================== */

.fullWidth {
width: 100%;
display: block;
}

/* ========================================== */
/* ERROR MESSAGE */
/* ========================================== */

.errorMessage {
font-size: var(--lufa-component-input-helper-text-font-size);
color: var(--lufa-component-input-helper-text-color-error);
}

/* ========================================== */
/* STANDALONE SELECTORS */
/* ========================================== */

/* Focus ring (keyboard navigation) */
.select:focus-visible {
border-color: var(--lufa-component-input-border-focus);
box-shadow: 0 0 0 var(--lufa-component-shared-focus-outline-width) var(--lufa-component-shared-focus-outline-color);
}

/* Error + Focus — red focus ring */
.select.error:focus-visible {
box-shadow: 0 0 0 var(--lufa-component-shared-focus-outline-width) var(--lufa-component-input-border-error);
}
Loading
Loading